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