P2JIndex.java

/*
** Module   : P2JIndex.java
** Abstract : Index helper class; represents a Progress or SQL index
**
** Copyright (c) 2005-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
** 001 ECF 20051010   @23180 Created initial version. Describes a database index and contains several helper
**                           methods to detect index redundancy and sets of minimal unique index component
**                           sets.
** 002 ECF 20060129   @24284 Changes needed for temp table support. Added convenience constructor and made
**                           table name a settable field after construction.
** 003 ECF 20070424   @33213 Fixed equals and hashCode implementations. Neither took index component order
**                           into account.
** 004 ECF 20080311   @37478 Added isChar parameter to addComponent(). Also integrated generics.
** 005 ECF 20090428   @42126 Exposed setUnique(boolean) as package private. Needed by DirtyTempTableHelper.
** 006 VMN 20130830          Added method getOrderByClause().
** 007 VMN 20131126          Fixed getOrderByClause(), removed to-do marker.
** 008 OM  20130821          Added descending support for index components.
** 009 SVL 20140106          Minor change in toString.
** 010 ECF 20140318          Added copy constructor, compact method.
** 011 ECF 20140914          Fixed minor problem with hashCode implementation.
** 012 ECF 20150201          Added components(boolean) method.
** 013 ECF 20200906          New ORM implementation.
** 014 OM  20200925          Separated namespaces for index components.
** 015 IAS 20201224          Added word tables support
**     IAS 20210219          Word tables support for custom extents
**     IAS 20210408          Added copy c'tor with table name replacement.
**     ECF 20220617          When eliminating redundant indices, consider component sort direction.
**     OM  20220609          Added support for legacy name.
**     SVL 20230110          P2JIndex.components() provides direct access to the components array in order to
**                           improve performance.
** 016 CA  20230221          Javadoc fixes.
** 017 GBB 20230512          Logging methods replaced by CentralLogger/ConversionStatus.
** 018 DDF 20240205          Added mandatory property to index components.
*/

/*
** 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 com.goldencode.p2j.persist.dialect.*;
import com.goldencode.p2j.util.logging.*;

/**
 * A descriptor of an index, which may be either a Progress index or an
 * SQL database index. The latter's features for our purposes here represent
 * a subset of the former's features, so the same class is useful for both.
 * <p>
 * An index is described at two levels:
 * <ol>
 *   <li>attributes of the index itself:
 *   <ul>
 *     <li>table name
 *     <li>index name
 *     <li>unique flag
 *     <li>primary flag
 *     <li>word flag
 *   </ul>
 *   <li>components of the index (1 or more):
 *   <ul>
 *     <li>field or column name
 *     <li>sort direction
 *     <li>abbreviation support
 *     <li>whether data type is character
 *     <li>whether case should be ignored for a character component
 *   </ul>
 * </ol>
 * <p>
 * This class contains several static convenience methods:
 * <ul>
 *   <li>{@link #normalizeUniqueIndexes}: Given a list of indexes for the
 *       same table, this method identifies the minimal set of unique indexes
 *       which define the least common denominator, unique indexes for a
 *       table. It also normalizes any index which contains all of the
 *       components of any of these by marking it as unique.
 *   <li>{@link #nonRedundantIndexes}:  Given a list of indexes for the same
 *       table, this method identifies the sub-list of indexes which are not
 *       redundant with one another. For our purposes here, an index is
 *       considered redundant with another, if both indexes have the same
 *       number of components, where each component appears in the same order
 *       and has the same name as its counterpart.
 * </ul>
 */
public final class P2JIndex
{
   /** Logger */
   private static final CentralLogger LOG = CentralLogger.get(P2JIndex.class);
   
   /** Name of index */
   private final String name;
   
   /** Is this the primary index for its table? */
   private boolean primary;
   
   /** Is this a word index? */
   private final boolean word;
   
   /** List of index components */
   private final ArrayList<P2JIndexComponent> components = new ArrayList<>();
   
   /** Name of table with which index is associated */
   private String table;
   
   /** Does this index define a unique constraint for its table? */
   private boolean unique;
   
   /** Word table name (for word index)*/
   private String wordTableName;
   
   /** The legacy name of the index. */
   private String legacyName;
   
   /**
    * Convenience constructor.
    *
    * @param   table
    *          Name of the table with which the index is associated.
    * @param   name
    *          Index name.
    * @param   unique
    *          <code>true</code> if this index defines a unique constraint
    *          for its table, else <code>false</code>.
    */
   public P2JIndex(String table, String name, boolean unique)
   {
      this(table, name, unique, false, false, null);
   }
   
   /**
    * Convenience constructor.
    *
    * @param   table
    *          Name of the table with which the index is associated.
    * @param   name
    *          Index name.
    * @param   unique
    *          <code>true</code> if this index defines a unique constraint
    *          for its table, else <code>false</code>.
    * @param   primary
    *          <code>true</code> if this index is the primary index for its
    *          table, else <code>false</code>.
    * @param   word
    *          <code>true</code> if this index is a word index, else
    *          <code>false</code>.
    */
   public P2JIndex(String table, String name, boolean unique, boolean primary, boolean word)
   {
      this(table, name, unique, primary, word, null);
   }
   
   /**
    * Convenience constructor.
    *
    * @param   table
    *          Name of the table with which the index is associated.
    * @param   name
    *          Index name.
    * @param   unique
    *          <code>true</code> if this index defines a unique constraint
    *          for its table, else <code>false</code>.
    * @param   primary
    *          <code>true</code> if this index is the primary index for its
    *          table, else <code>false</code>.
    * @param   word
    *          <code>true</code> if this index is a word index, else
    *          <code>false</code>.
    * @param   wordTableName
    *          Word table name (only used for word index).
    */
   public P2JIndex(String table,
                   String name,
                   boolean unique, 
                   boolean primary,
                   boolean word,
                   String wordTableName)
   {
      this.table = table;
      this.name = name;
      this.unique = unique;
      this.primary = primary;
      this.word = word;
      this.wordTableName = wordTableName;
      
      // assume the name is the legacy name. If not, it will be overwritten using the setter
      legacyName = name;
   }
   
   /**
    * Copy constructor.
    * 
    * @param   index
    *          Instance to copy.
    */
   public P2JIndex(P2JIndex index)
   {
      this(index.table, index.name, index.unique, index.primary, index.word, null);

      ArrayList<P2JIndexComponent> components = index.components;
      for (int i = 0; i < components.size(); i++)
      {
         P2JIndexComponent comp = components.get(i);
         this.components.add(new P2JIndexComponent(comp));
      }
      
      compact();
   }
   
   /**
    * Copy constructor with table name replacement.
    * 
    * @param   index
    *          Instance to copy.
    * @param   tableName
    *          table name replacement.
    */
   public P2JIndex(P2JIndex index, String tableName)
   {
      this(tableName, index.name, index.unique, index.primary, index.word, null);

      ArrayList<P2JIndexComponent> components = index.components;
      for (int i = 0; i < components.size(); i++)
      {
         P2JIndexComponent comp = components.get(i);
         this.components.add(new P2JIndexComponent(comp));
      }
      
      compact();
   }

   /**
    * Given a list of indexes, identify the minimal set of unique indexes
    * which define the least common denominator, unique constraints for a
    * table. This will be the set of unique indexes which are least specific
    * in terms of components. For instance, given three unique indexes with
    * the following components:
    * <ul>
    *   <li>Index 1:  A
    *   <li>Index 2:  A, B
    *   <li>Index 3:  A, B, C
    * </ul>
    * Index 1 is the least common denominator, unique index of the group.
    * This is because the table can contain no tuples where the value of
    * columns B or C affect the uniqueness of the tuple. That is, for every
    * unique value of A, there can be only one combination of B and C, since
    * no other tuple can exist with the same value of A but different values
    * of B or C.
    * <p>
    * Conversely, if Index 2 and Index 3 are not explicitly defined as being
    * unique in a Progress schema, they are nevertheless <i>implicitly</i>
    * unique because they include column A, which requires each tuple to
    * contain a unique value for column A. Thus, as a useful side effect,
    * this method normalizes such indexes by marking them unique, regardless
    * of whether the unique flag was applied in the originating Progress
    * schema.
    *
    * @param   indexes
    *          A list of indexes for a common table.
    * @param   namespace
    *          Flag for data namespace to be processed. See int constants defined in {@code P2JIndexComponent}
    *          for valid values. If there is no match, an exception is thrown. 
    *
    * @return  The list of indexes which defines the minimal set of least common denominator, unique indexes
    *          for that index group. Order of the original list is not guaranteed to be preserved in the
    *          resulting subset list.
    *
    * @throws  IllegalArgumentException
    *          if {@code indexes} contains index descriptors for different tables.
    */
   public static List<P2JIndex> normalizeUniqueIndexes(List<P2JIndex> indexes, int namespace)
   {
      Map<Set<String>, P2JIndex> results = new HashMap<>();
      int size = indexes.size();
      
      // Iterate backwards through the full list to get the next index to
      // test as a least common denominator candidate.
      ListIterator<P2JIndex> iIter = indexes.listIterator(size);
      while (iIter.hasPrevious())
      {
         P2JIndex iIndex = iIter.previous();
         boolean minimal = iIndex.isUnique();
         
         // Iterate backwards through the full list to test each other index
         // in the list against the LCD candidate.
         ListIterator<P2JIndex> jIter = indexes.listIterator(size);
         while (jIter.hasPrevious())
         {
            P2JIndex jIndex = jIter.previous();
            
            // Skip the same descriptor instance.
            if (iIndex == jIndex)
            {
               continue;
            }
            
            // Assert that both test subjects are from the same table.
            if (!iIndex.getTable().equals(jIndex.getTable()))
            {
               throw new IllegalArgumentException(
                  "Cannot normalize indexes from different tables;  found '" + iIndex + "' and '" + jIndex);
            }
            
            // Is the LCD candidate a possible superset of another index?
            if (iIndex.containsAll(jIndex, namespace))
            {
               // the normalization step:  if the LCD candidate contains all components of a unique index, it
               // is itself unique, by definition.
               if (jIndex.isUnique())
               {
                  iIndex.setUnique(true);
                  minimal = true;
               }
               
               // Candidate cannot be confirmed if:
               // A) its components form a superset of another index' components;  OR
               // B) neither it nor the counterpart index is unique.
               int iSize = iIndex.size();
               int jSize = jIndex.size();
               if ((iSize > jSize && jIndex.isUnique()) || (!minimal && !jIndex.isUnique()))
               {
                  minimal = false;
               }
               
               // no need for further consideration if we've already ruled out LCDness
               if (!minimal)
               {
                  break;
               }
            }
         }
         
         // If we got through both loop levels and the candidate was
         // confirmed, store it in a map, keyed by the candidate's name set.
         if (minimal)
         {
            results.put(iIndex.getNameSet(null, namespace), iIndex);
         }
      }
      
      return new ArrayList<>(results.values());
   }
   
   /**
    * Given a list of index descriptors, assemble a list of those which are
    * not redundant with one another.  An index is considered redundant if it
    * contains all components of another, and in the same order.  For every
    * group of redundant indexes, one (the first encountered in a first to
    * last scan) is added to the resulting, non-redundant list.
    *
    * @param   indexes
    *          List of indexes which contain possible redundancies.
    *
    * @return  Sublist of <code>indexes</code> which contains no redundancies.
    */
   public static List<P2JIndex> nonRedundantIndexes(List<P2JIndex> indexes)
   {
      List<P2JIndex> list = new ArrayList<>();
      int size = indexes.size();
      for (int i = 0; i < size; i++)
      {
         P2JIndex iIndex = indexes.get(i);
         
         boolean redundant = false;
         
         for (int j = 0; j < size; j++)
         {
            if (j == i)
            {
               continue;
            }
            
            P2JIndex jIndex = indexes.get(j);
            int iSize = iIndex.size();
            int jSize = jIndex.size();
            
            if (iSize == jSize && iIndex.leadsWith(jIndex) && i > j)
            {
               redundant = true;
               break;
            }
         }
         
         if (!redundant)
         {
            list.add(iIndex);
         }
      }
      
      return list;
   }
   
   /**
    * Get table name associated with this index.
    *
    * @return  Table name.
    */
   public String getTable()
   {
      return table;
   }
   
   /**
    * Get name of this index.
    *
    * @return  Index name.
    */
   public String getName()
   {
      return name;
   }
   
   /**
    * Indicate whether this index is unique.
    *
    * @return  <code>true</code> if unique, else <code>false</code>.
    */
   public boolean isUnique()
   {
      return unique;
   }
   
   /**
    * Indicate whether this index is the primary index for its table (only meaningful for Progress indexes).
    *
    * @return  {@code true} if primary, else {@code false}.
    */
   public boolean isPrimary()
   {
      return primary;
   }
   
   /**
    * Indicate whether this index is the primary index for its table (only meaningful for Progress indexes).
    *
    * @param    primary  
    *           {@code true} for marking the index as primary, else {@code false}.
    */
   public void setPrimary(boolean primary)
   {
      this.primary =  primary;
   }
   
   /**
    * Indicate whether this index is a word index (only meaningful for
    * Progress indexes).
    *
    * @return  <code>true</code> if a word index, else <code>false</code>.
    */
   public boolean isWord()
   {
      return word;
   }
   
   /**
    * Add an index component to the index definition. This overloaded variant will create a
    * <strong>generic</strong> named component always ascending, non-character and non-abbreviated. 
    * <p>
    * Because this method creates a generic component, all its namespaces are assigned to same value. Usage of
    * this method is <strong>not</strong> encouraged because of the possible leaks of the data from one
    * namespace to another. 
    *
    * @param   name
    *          The legacy name of the index field which defines this index component.
    * 
    * @return  Newly added {@code P2JIndexComponent} object.
    */
   public P2JIndexComponent addComponent(String name)
   {
      return addComponent(name, null, null, false, false, false, false, false);
   }
   
   /**
    * Add an index component to the index definition. This overloaded variant will create a
    * <strong>generic</strong> named component always ascending, non-abbreviated.
    * <p>
    * Because this method creates a generic component, all its namespaces are assigned to same value. Usage of
    * this method is <strong>not</strong> encouraged because of the possible leaks of the data from one
    * namespace to another. 
    *
    * @param   name
    *          The name of the index field which defines this index component.
    * @param   ignoreCase
    *          {@code true} if this component represents a character field AND case is ignored when sorting
    *          index, else {@code false}.
    * 
    * @return  Newly added {@code P2JIndexComponent} object.
    */
   public P2JIndexComponent addComponent(String name, boolean ignoreCase)
   {
      return addComponent(name, name, name, false, true, ignoreCase, false, false);
   }
   
   /**
    * Add an index component to the index definition. An index component consists of a field name, sort
    * direction, case sensitivity flag, abbreviation flag, and mandatory flag.
    *
    * @param   legacy
    *          The legacy name (optional).
    * @param   property
    *          Property name (optional).
    * @param   column
    *          Column name (optional).
    * @param   descending
    *          {@code true} if this component sorts naturally in descending order; {@code false} to sort in
    *          ascending order.
    * @param   isChar
    *          {@code true} if this component represents a character field.
    * @param   ignoreCase
    *          {@code true} if this component represents a character field AND case is ignored when sorting
    *          index, else {@code false}.
    * @param   abbreviated
    *          {@code true} if this component is abbreviated, else {@code false}.
    * 
    * @return  Newly added {@code P2JIndexComponent} object.
    */
   public P2JIndexComponent addComponent(String legacy,
                                         String property,
                                         String column,
                                         boolean descending,
                                         boolean isChar,
                                         boolean ignoreCase,
                                         boolean abbreviated)
   {
      return addComponent(legacy, property, column, descending, isChar, ignoreCase, abbreviated, false);
   }
   
   /**
    * Add an index component to the index definition. An index component consists of a field name, sort
    * direction, case sensitivity flag, abbreviation flag, and mandatory flag.
    *
    * @param   legacy
    *          The legacy name (optional).
    * @param   property
    *          Property name (optional).
    * @param   column
    *          Column name (optional).
    * @param   descending
    *          {@code true} if this component sorts naturally in descending order; {@code false} to sort in
    *          ascending order.
    * @param   isChar
    *          {@code true} if this component represents a character field.
    * @param   ignoreCase
    *          {@code true} if this component represents a character field AND case is ignored when sorting
    *          index, else {@code false}.
    * @param   abbreviated
    *          {@code true} if this component is abbreviated, else {@code false}.
    * @param   isMandatory
    *          {@code true} if this component is mandatory (non-null), else {@code false}.
    * 
    * @return  Newly added {@code P2JIndexComponent} object.
    */
   public P2JIndexComponent addComponent(String legacy,
                                         String property,
                                         String column,
                                         boolean descending,
                                         boolean isChar,
                                         boolean ignoreCase,
                                         boolean abbreviated,
                                         boolean isMandatory)
   {
      return addComponent(legacy,
                          0,
                          property,
                          column,
                          descending,
                          isChar,
                          ignoreCase,
                          abbreviated,
                          isMandatory);
   }
   
   /**
    * Add an index component to the index definition. An index component consists of a field name, sort
    * direction, case sensitivity flag, abbreviation flag, and mandatory flag.
    *
    * @param   legacy
    *          The legacy name (optional).
    * @param   extent 
    *          Extent of the component (can be > 0 only for a word index component).
    * @param   property
    *          Property name (optional).
    * @param   column
    *          Column name (optional).
    * @param   descending
    *          {@code true} if this component sorts naturally in descending order; {@code false} to sort in
    *          ascending order.
    * @param   isChar
    *          {@code true} if this component represents a character field.
    * @param   ignoreCase
    *          {@code true} if this component represents a character field AND case is ignored when sorting
    *          index, else {@code false}.
    * @param   abbreviated
    *          {@code true} if this component is abbreviated, else {@code false}.
    * @param   isMandatory
    *          {@code true} if this component is mandatory (non-null), else {@code false}.
    * 
    * @return  Newly added {@code P2JIndexComponent} object.
    */
   public P2JIndexComponent addComponent(String legacy,
                                         int extent, 
                                         String property,
                                         String column,
                                         boolean descending,
                                         boolean isChar,
                                         boolean ignoreCase,
                                         boolean abbreviated,
                                         boolean isMandatory)
   {
      P2JIndexComponent comp = new P2JIndexComponent(
            legacy, property, column, extent, descending, isChar, ignoreCase, abbreviated, isMandatory);
      components.add(comp);
      
      return comp;
   }
   
   /**
    * Get the number of components in this index.
    *
    * @return  Index size.
    */
   public int size()
   {
      return components.size();
   }
   
   /**
    * Reclaim a bit of memory once all components have been added and this object will no longer
    * be changed.
    */
   public void compact()
   {
      ((ArrayList<P2JIndexComponent>) components).trimToSize();
   }
   
   /**
    * Get the index component at the specified position.
    *
    * @param   position
    *          0-based position of the desired index component.
    *
    * @return  Index component at <code>position</code>.
    *
    * @throws  ArrayIndexOutOfBoundsException
    *          if <code>position</code> is invalid.
    */
   public P2JIndexComponent getComponent(int position)
   {
      return components.get(position);
   }
   
   /**
    * Get all {@link P2JIndexComponent}s in this index.
    *
    * @return  Array of components.
    */
   public ArrayList<P2JIndexComponent> components()
   {
      return components;
   }

   /**
    * Get an iterator on all {@link P2JIndexComponent}s in this index.
    *
    * @return  Component iterator.
    */
   public Iterator<P2JIndexComponent> componentsIter()
   {
      return components.iterator();
   }
   
   /**
    * Get an iterator on the {@link P2JIndexComponent}s in this index, optionally omitting a
    * trailing primary key component, if present and if the index is non-unique.
    * 
    * @param   omitTrailingPK
    *          <code>true</code> to omit the trailing primary key component, if present and the
    *          index is non-unique; else <code>false</code>.
    * 
    * @return  Component iterator.
    */
   public Iterator<P2JIndexComponent> components(final boolean omitTrailingPK)
   {
      if (unique || !omitTrailingPK)
      {
         // return regular iterator if index is explicitly unique or we've been told not to omit
         // a trailing primary key component
         return components.iterator();
      }
      
      // return a custom iterator which ignores a trailing primary key component
      return new Iterator<P2JIndexComponent>()
      {
         private final Iterator<P2JIndexComponent> iter = components.iterator();
         
         private P2JIndexComponent next = null;
         
         public boolean hasNext()
         {
            if (iter.hasNext() && next == null)
            {
               next = iter.next();
               
               // ignore the last component if it's the primary key
               if (!iter.hasNext() && DatabaseManager.PRIMARY_KEY.equals(next.getPropertyName()))
               {
                  next = null;
               }
            }
            
            return (next != null);
         }
         
         public P2JIndexComponent next()
         {
            if (!hasNext())
            {
               throw new NoSuchElementException();
            }
            
            P2JIndexComponent comp = next;
            next = null;
            
            return comp;
         }
         
         public void remove()
         {
            // cannot remove components with this iterator
            throw new UnsupportedOperationException();
         }
      };
   }
   
   /**
    * Return up to <code>size</code> leading components of this index. If
    * <code>size</code> is larger than the number of components in the index,
    * all components are returned. The order of components is preserved in
    * the returned list.
    *
    * @param   size
    *          Number of leading components to return.
    *
    * @return  List view of the first <code>size</code> components of the
    *          index.
    */
   public List<P2JIndexComponent> getLeadingComponents(int size)
   {
      size = Math.min(size, components.size());
      
      return components.subList(0, size);
   }

   /**
    * Get order by clause for given index of database.
    *
    * @param   dmoAlias
    *          Alias qualifier which represents DMO in sort phrase.
    * @param   namespace
    *          The namespace of the components to be used when constructing the returned {@code String}.
    *          For possible values see the {@link P2JIndexComponent} integer namespace constants.
    *
    * @return  order by clause.
    */
   public String getOrderByClause(String dmoAlias, int namespace)
   {
      StringBuilder result = new StringBuilder();
      for (int i = 0; i < components.size(); i++)
      {
         P2JIndexComponent nextComp = components.get(i);
         if (result.length() > 0)
         {
            result.append(", ");
         }

         String field = nextComp.getName(namespace);
            
         result.append(dmoAlias);
         result.append(".");
         result.append(field);
         result.append(" ");
         result.append(nextComp.isDescending() ? "desc" : "asc");
      }
      return result.toString();
   }
   
   /**
    * Test whether this index leads with components of the same name as the specified index. For the purpose
    * of this test, only the names of the components are considered;  the sort direction and word flags are
    * ignored. The order of the index components is significant.
    *
    * @param   index
    *          Index with which to compare this index' components.
    *
    * @return  {@code true} if and only if this index contains <b>all</b> of the components of {@code index}
    *          as its leading components, and these components appear in the same order in both indexes;
    *          {@code false} otherwise.
    */
   public boolean leadsWith(P2JIndex index)
   {
      int size = index.size();
      if (size > components.size())
      {
         return false;
      }
      
      for (int i = 0; i < size; i++)
      {
         P2JIndexComponent next = index.components.get(i);
         P2JIndexComponent local = components.get(i);
         
         if (!next.sameAs(local, true))
         {
            return false;
         }
      }
      
      return true;
   }
   
   /**
    * Return the set of index component names for this index.  Iterations over this set will return the names
    * in the order in which they were added to the index.
    *
    * @param   dialect
    *          Optional P2J database dialect.
    *          <ul>
    *             <li>If provided, index components of text type will be processed as necessary to produce an
    *                 expression which will right trim whitespace, properly handle case insensitivity and
    *                 sorting order (descending) as necessary.  This may mean wrapping the property name in
    *                 various functions, substituting the name with a computed column name, or whatever is
    *                 appropriate for the particular dialect.
    *             <li>Otherwise only the plain list of components names is returned.
    *          </ul> 
    * @param   namespace
    *          Flag for data namespace to be processed. See int constants defined in {@code P2JIndexComponent}
    *          for valid values. If there is no match, an exception is thrown. 
    *
    * @return  Set of index component names.
    */
   public LinkedHashSet<String> getNameSet(Dialect dialect, int namespace)
   {
      LinkedHashSet<String> set = new LinkedHashSet<>();
      for (int i = 0; i < components.size(); i++)
      {
         P2JIndexComponent next = components.get(i);
         String name = next.getName(namespace);
         if (dialect != null)
         {
            if (next.isCharType())
            {
               // for char type wrap it into functions according to dialect
               boolean ignoreCase = next.isIgnoreCase();
               name = dialect.getProcessedCharacterColumnName(name, ignoreCase);
            }
            
            // check sorting order, this applies for all BDTs, not only char
            if (next.isDescending())
            {
               name += " desc";
            }
         }
         set.add(name);
      }
      
      return set;
   }
   
   /**
    * Obtain the legacy name of the index.
    * 
    * @return  the legacy name of the index. If not expressly set, this information might not be accurate.
    */
   public String getLegacyName()
   {
      return legacyName;
   }
   
   /**
    * Sets the legacy name of the index, when available.
    * 
    * @param   legacyName
    *          The legacy name of the index.
    */
   public void setLegacyName(String legacyName)
   {
      this.legacyName = legacyName;
   }
   
   /**
    * Implement a very loose test for equivalence, whereby <code>o</code> is
    * considered equivalent to this object as long as both instances contain
    * exactly the same set of component names, in the same order. Table name,
    * index name, and component sort direction and abbreviation support are
    * ignored for this test.
    *
    * @param   o
    *          An object instance, presumably a <code>P2JIndex</code> object,
    *          to test for equivalence with this object.
    *
    * @return  <code>true</code> if <code>o</code> tests as equivalent to
    *          this object, using the criteria specified above, else
    *          <code>false</code>.
    */
   public boolean equals(Object o)
   {
      if (!(o instanceof P2JIndex))
      {
         return false;
      }
      P2JIndex idx = (P2JIndex) o;
      
      // Can't use LinkedHashSet.equals() to compare name sets returned by getNameSet(), because that equals()
      // implementation does not take element order into account.  Do the comparison manually.
      
      ArrayList<P2JIndexComponent> c1 = this.components;
      ArrayList<P2JIndexComponent> c2 = idx.components;
      
      if (c1.size() != c2.size())
      {
         return false;
      }

      for (int i = 0; i < c1.size(); i++)
      {
         P2JIndexComponent comp1 = c1.get(i);
         P2JIndexComponent comp2 = c2.get(i);

         if (!comp1.equals(comp2))
         {
            return false;
         }
      }
      
      return true;
   }
   
   /**
    * Overridden implementation to ensure consistency with our specialized
    * {@link #equals} method implementation.  Note that only the index
    * component names are taken into account;  all other index and index
    * component data is ignored for purposes of this calculation.
    *
    * @return  Hash code of this index.
    */
   public int hashCode()
   {
      int result = 17;
      for (int i = 0; i < components.size(); i++)
      {
         P2JIndexComponent next = components.get(i);
         result = 37 * result + next.hashCode();
      }
      
      return result;
   }
   
   /**
    * 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()
   {
      StringBuilder buf = new StringBuilder();
      
      if (table != null)
      {
         buf.append(table);
         buf.append('.');
      }
      buf.append(name);
      buf.append(" [");
      
      for (int i = 0; i < components.size(); i++)
      {
         P2JIndexComponent component = components.get(i);
         if (i > 0)
         {
            buf.append(", ");
         }
         buf.append(component);
      }
      
      buf.append(']');
      
      if (isUnique())
      {
         buf.append('*');
      }
      
      if (isPrimary())
      {
         buf.append('!');
      }
      
      if (isWord())
      {
         buf.append('%');
      }
      
      return buf.toString();
   }
   
   /**
    * Get word table names by dialect
    * 
    * @return  Word table name.
    */
   public String getWordTableName()
   {
      return wordTableName;
   }

   /**
    * Set word table names
    * 
    * @param   wordTableName
    *          Word table names by dialect.
    */
   public void setWordTableName(String wordTableName)
   {
      this.wordTableName = wordTableName;
   }

   /**
    * Set the table with which this index is associated.
    *
    * @param   table
    *          Name of table.
    */
   void setTable(String table)
   {
      this.table = table;
   }
   
   /**
    * Set the unique flag within this index.
    *
    * @param   unique
    *          <code>true</code> to indicate that this index is unique;
    *          <code>false</code> to indicate it is not.
    */
   void setUnique(boolean unique)
   {
      this.unique = unique;
   }
   
   /**
    * Indicate whether this index contains all of the components of the specified index.  Only index component
    * names are considered, not any other properties of each component.
    *
    * @param   index
    *          Index whose components are checked against those in this index.
    * @param   namespace
    *          Flag for data namespace to be processed. See int constants defined in {@code P2JIndexComponent}
    *          for valid values. If there is no match, an exception is thrown. 
    *
    * @return  {@code true} if this object contains all components of {@code index};  else {@code false}.
    */
   private boolean containsAll(P2JIndex index, int namespace)
   {
      Set<String> localSet = getNameSet(null, namespace);
      Set<String> otherSet = index.getNameSet(null, namespace);
      
      return localSet.containsAll(otherSet);
   }
   
   /**
    * Test harness. Creates multiple instance of this class and tests that
    * minimal unique indexes can be determined, and that redundant indexes
    * can be identified and culled from a list.
    *
    * @param   args
    *          Not used.
    */
   public static void main(String[] args)
   {
      try
      {
         P2JIndex index;
         List<P2JIndex> list = new ArrayList<>();
         
         index = new P2JIndex("Table 1", "ABCD", true, false, false);
         index.addComponent("A");
         index.addComponent("B");
         index.addComponent("C");
         index.addComponent("D");
         list.add(index);
         
         index = new P2JIndex("Table 1", "ACB", true, false, false);
         index.addComponent("A");
         index.addComponent("C");
         index.addComponent("B");
         list.add(index);
         
         index = new P2JIndex("Table 1", "BCA", true, false, false);
         index.addComponent("B");
         index.addComponent("C");
         index.addComponent("A");
         list.add(index);
         
         index = new P2JIndex("Table 1", "DE", true, false, false);
         index.addComponent("D");
         index.addComponent("E");
         list.add(index);
         
         index = new P2JIndex("Table 1", "BAC", true, false, false);
         index.addComponent("B");
         index.addComponent("A");
         index.addComponent("C");
         list.add(index);
         
         index = new P2JIndex("Table 1", "BC", true, false, false);
         index.addComponent("B");
         index.addComponent("C");
         list.add(index);
         
         index = new P2JIndex("Table 1", "AB", true, false, false);
         index.addComponent("A");
         index.addComponent("B");
         list.add(index);
         
         index = new P2JIndex("Table 1", "F1", false, false, false);
         index.addComponent("F");
         list.add(index);
         
         index = new P2JIndex("Table 1", "F2", true, true, false);
         index.addComponent("F");
         list.add(index);
         
         index = new P2JIndex("Table 1", "ED1", false, false, false);
         index.addComponent("E");
         index.addComponent("D");
         list.add(index);
         
         index = new P2JIndex("Table 1", "ED2", false, false, false);
         index.addComponent("E");
         index.addComponent("D");
         list.add(index);
         
         index = new P2JIndex("Table 1", "ED3", true, false, false);
         index.addComponent("E");
         index.addComponent("D");
         list.add(index);
         
         index = new P2JIndex("Table 1", "EG", false, false, false);
         index.addComponent("E");
         index.addComponent("G", true);
         list.add(index);
         
         index = new P2JIndex("Table 1", "CAB", true, false, false);
         index.addComponent("C");
         index.addComponent("A");
         index.addComponent("B");
         list.add(index);
         
         System.out.println("PRE-NORMALIZED (" + list.size() + ")");
         System.out.println("-------------------------------");
         System.out.println(list);
         System.out.println();
         
         List<P2JIndex> minimalUnique = P2JIndex.normalizeUniqueIndexes(list, P2JIndexComponent.LEGACY);
         List<P2JIndex> nonRedundant = P2JIndex.nonRedundantIndexes(list);
         
         System.out.println("NON-REDUNDANT (" + nonRedundant.size() + ")");
         System.out.println("-------------------------------");
         System.out.println(nonRedundant);
         System.out.println();
         
         System.out.println("MINIMAL UNIQUE (" + minimalUnique.size() + ")");
         System.out.println("-------------------------------");
         System.out.println(minimalUnique);
         System.out.println();
         
         Set<P2JIndex> remaining = new HashSet<>(nonRedundant);
         remaining.removeAll(minimalUnique);
         System.out.println("REMAINING (" + remaining.size() + ")");
         System.out.println("-------------------------------");
         System.out.println(remaining);
         System.out.println();
         
         System.out.println("NORMALIZED (" + list.size() + ")");
         System.out.println("-------------------------------");
         System.out.println(list);
         System.out.println();
      }
      catch (Exception exc)
      {
         LOG.severe("", exc);
      }
   }
}