P2JIndexComponent.java

/*
** Module   : P2JIndexComponent.java
** Abstract : Index helper class; represents a Progress or SQL index component
**
** Copyright (c) 2004-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
** 001 ECF 20051010   @23179 Created initial version. Represents a single component of a database
**                           index.
** 002 ECF 20070328   @32646 Partial fix for text components of an index. The getNameCaseAware()
**                           method was not embedding the rtrim() function inside the upper()
**                           function.
** 003 ECF 20070328   @32647 Need a more complete solution for the getNameCaseAware() method. The
**                           previous fix does not properly wrap a component with the rtrim()
**                           function for case-sensitive text components. We need more type
**                           information for an index component in order to do that.
** 004 ECF 20070629   @35300 Minor optimization. Replaced StringBuffer with StringBuilder.
** 005 ECF 20080311   @37479 Added flag to indicate whether component is a character data type.
**                           Also optimized flags into a single bitfield.
** 006 ECF 20140318          Added copy constructor.
** 007 CA  20190724          The field name must be case-insensitive.
**     OM  20190829          Stored the original name and provided when case-sensitive name
**                           is required.
** 008 ECF 20200906          Javadoc fixes.
** 009 OM  20200924          Objects of this class carry multiple information to avoid map lookups for them.
**                           Made object immutable and local optimized.
** 019 IAS 20210219          Word tables support for custom extents
** 020 GBB 20230512          Logging methods replaced by CentralLogger/ConversionStatus.
** 021 DDF 20240205          Added the MANDATORY flag.
*/

/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
** 
** Additional terms under GNU Affero GPL version 3 section 7:
** 
**   Under Section 7 of the GNU Affero GPL version 3, the following additional
**   terms apply to the works covered under the License.  These additional terms
**   are non-permissive additional terms allowed under Section 7 of the GNU
**   Affero GPL version 3 and may not be removed by you.
** 
**   0. Attribution Requirement.
** 
**     You must preserve all legal notices or author attributions in the covered
**     work or Appropriate Legal Notices displayed by works containing the covered
**     work.  You may not remove from the covered work any author or developer
**     credit already included within the covered work.
** 
**   1. No License To Use Trademarks.
** 
**     This license does not grant any license or rights to use the trademarks
**     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
**     of Golden Code Development Corporation. You are not authorized to use the
**     name Golden Code, FWD, or the names of any author or contributor, for
**     publicity purposes without written authorization.
** 
**   2. No Misrepresentation of Affiliation.
** 
**     You may not represent yourself as Golden Code Development Corporation or FWD.
** 
**     You may not represent yourself for publicity purposes as associated with
**     Golden Code Development Corporation, FWD, or any author or contributor to
**     the covered work, without written authorization.
** 
**   3. No Misrepresentation of Source or Origin.
** 
**     You may not represent the covered work as solely your work.  All modified
**     versions of the covered work must be marked in a reasonable way to make it
**     clear that the modified work is not originating from Golden Code Development
**     Corporation or FWD.  All modified versions must contain the notices of
**     attribution required in this license.
*/

package com.goldencode.p2j.persist;

import com.goldencode.p2j.util.logging.*;

import java.util.*;

/**
 * Represents a single index component; stores information about the component's name, sort direction, whether
 * it contains text data, its case sensitivity, and abbreviated status. The purpose of the name attribute is
 * left intentionally vague, such that it could represent a Progress field name, or an SQL database table
 * column name, or a Java DMO property name.
 */
public final class P2JIndexComponent
{
   /** Logger */
   private static final CentralLogger LOG = CentralLogger.get(P2JIndexComponent.class);
   
   /** Flag indicating descending (non-default) sort direction */
   private static final int DESCENDING = 0x01;
   
   /** Flag indicating component represents character data */
   private static final int CHARACTER = 0x02;
   
   /** Flag indicating component ignores case (character data only) */
   private static final int IGNORE_CASE = 0x04;
   
   /** Flag indicating abbreviated status (Progress semantic) */
   private static final int ABBREVIATED = 0x08;
   
   /** Flag indicating if the component is non-null */
   private static final int MANDATORY = 0x10;
   
   /** Name of indexed property (if available). */
   private final String propertyName;
   
   /** Name of indexed database column (if available). */
   private final String columnName;
   
   /** The legacy name of this component was built from (case sensitive), if available. */
   private final String legacyName;
   
   /** The legacy normalized name of this component was built from, if available. */
   private final String normalizedName;
   
   /** Extent of the component (can be > 0 only for a word index component) */
   private final int extent;
   /**
    * Flag for namespaces supported by this object. If this identifier is on then the 4GL legacy name is
    * present (and the normalized name, evidently).
    */
   public static final int LEGACY = 1;
   
   /**
    * Flag for namespaces supported by this object. If this identifier is on then the ORM property name is
    * present (usually the column is also present, but not mandatory).
    */
   public static final int PROPERTY = 2;
   
   /**
    * Flag for namespaces supported by this object. If this identifier is on then the SQL column name is
    * present.
    */
   public static final int COLUMN = 4;
   
   /**
    * Flag for namespaces supported by this object. If this identifier is on then the normalized 4GL legacy
    * name is present (and the case sensitive name, evidently).
    */
   public static final int NORMALIZED = 8;
   
   /** Property flags */
   private final int flags;
   
   /** Precomputed hash value. */
   private final int hash;
   
   /**
    * Constructor.
    *
    * @param   legacy
    *          The legacy name (optional).
    * @param   property
    *          Property name (optional).
    * @param   column
    *          Column name (optional).
    * @param   extent 
    *          Extent of the component (can be > 0 only for a word index component).
    * @param   descending
    *          {@code true} if component has non-default, descending sort direction;  {@code false} if
    *          ascending.
    * @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 index component supports the Progress semantic of using abbreviated data for a
    *          sort or selection match. Valid only if this is the last component of the index (not enforced
    *          here).
    * @param   isMandatory
    *          {@code true} if the index component is non-null, {@code false} otherwise.
    */
   public P2JIndexComponent(String legacy,
                            String property,
                            String column,
                            int extent,
                            boolean descending,
                            boolean isChar,
                            boolean ignoreCase,
                            boolean abbreviated,
                            boolean isMandatory)
   {
      // validation: all names cannot be simultaneously null:
      if (property == null && column == null && legacy == null)
      {
         throw new IllegalArgumentException("All index component names cannot be simultaneously null");
      }
      
      int flags = 0;
      if (descending)
      {
         flags |= DESCENDING;
      }
      if (isChar)
      {
         flags |= CHARACTER;
      }
      if (ignoreCase)
      {
         if (!isChar)
         {
            throw new IllegalArgumentException("Non-character index component cannot be case-insensitive");
         }
         flags |= IGNORE_CASE;
      }
      if (abbreviated)
      {
         flags |= ABBREVIATED;
      }
      if (isMandatory)
      {
         flags |= MANDATORY;
      }
      
      this.propertyName = property;
      this.columnName = column;
      this.legacyName = legacy;
      this.extent = extent;
      this.normalizedName = legacy == null ? null : legacy.toLowerCase();
      this.flags = flags;
      
      int hash0 = propertyName != null ? propertyName.hashCode() : 0;
      hash0 = 31 * hash0 + (columnName != null ? columnName.hashCode() : 0);
      hash0 = 31 * hash0 + (normalizedName != null ? normalizedName.hashCode() : 0);
      this.hash = 31 * hash0 + flags;
   }
   
   /**
    * Copy constructor.
    * 
    * @param   component
    *          Instance to copy.
    */
   public P2JIndexComponent(P2JIndexComponent component)
   {
      this.propertyName = component.propertyName;
      this.columnName = component.columnName;
      this.legacyName = component.legacyName;
      this.normalizedName = component.normalizedName;
      this.flags = component.flags;
      this.hash = component.hash;
      this.extent = component.extent;
   }
   
   /**
    * Constructor.
    *
    * @param   legacy
    *          The legacy name (optional).
    * @param   property
    *          Property name (optional).
    * @param   column
    *          Column name (optional).
    * @param   descending
    *          {@code true} if component has non-default, descending sort direction;  {@code false} if
    *          ascending.
    * @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 index component supports the Progress semantic of using abbreviated data for a
    *          sort or selection match. Valid only if this is the last component of the index (not enforced
    *          here).
    * @param   isMandatory
    *          {@code true} if the index component is non-null, {@code false} otherwise.
    */
   public P2JIndexComponent(String legacy,
                            String property,
                            String column,
                            boolean descending,
                            boolean isChar,
                            boolean ignoreCase,
                            boolean abbreviated,
                            boolean isMandatory)
   {
      this(legacy, property, column, 0, descending, isChar, ignoreCase, abbreviated, isMandatory);
   }
   /**
    * Returns one of the known flavours of this component.
    * 
    * @param   namespace
    *          The flag for one of the namespace to be returned.
    *
    * @return  The name of the component in the requested name space.
    * 
    * @throws  IllegalArgumentException
    *          if the parameter is not a simple namespace flag (no combinations allowed).
    * @throws  RuntimeException
    *          (temporary) If the requested name is not set. This normally point out a programming error
    *          as the caller should know the namespace this object was constructed with and the name spaces
    *          for them may be different (ex: Legacy: {@code a-Field}, Property: {@code aField}, SQL Column:
    *          {@code a_field}, Normalized legacy: {@code a-field}). 
    */
   public String getName(int namespace)
   {
      switch (namespace)
      {
         case LEGACY: return getLegacyName();
         case PROPERTY: return getPropertyName();
         case COLUMN: return getColumnName();
         case NORMALIZED: return getNormalizedName();
         default: throw new IllegalArgumentException("Invalid P2JIndexComponent namespace");
      }
   }
   
   /**
    * Get index legacy's name (in lowercase). To get the case-sensitive name use {@link #getLegacyName}.
    *
    * @return  Index field or column name (in lower case).
    */
   public String getNormalizedName()
   {
      checkNotNull(normalizedName, "normalizedName");
      return normalizedName;
   }
   
   /**
    * Get index component legacy's original name (case-sensitive).
    *
    * @return  Index component legacy's original name (case-sensitive).
    */
   public String getLegacyName()
   {
      checkNotNull(legacyName, "legacyName");
      return legacyName;
   }
   
   /**
    * Get index component's column name.
    *
    * @return  Index column name.
    */
   public String getColumnName()
   {
      checkNotNull(columnName, "columnName");
      return columnName;
   }
   
   /**
    * Get index property's column name.
    *
    * @return  Index property name.
    */
   public String getPropertyName()
   {
      checkNotNull(propertyName, "propertyName");
      return propertyName;
   }
   
   /**
    * Get extent of the field.
    *
    * @return  Extent of the field.
    */
   public int getExtent()
   {
      return extent;
   }

   /**
    * Indicate if index component uses descending (non-default) sort direction.
    *
    * @return  {@code true} if sort direction is descending, else {@code false} if ascending.
    */
   public boolean isDescending()
   {
      return ((flags & DESCENDING) != 0);
   }
   
   /**
    * Indicate if index component represents a character field.
    *
    * @return  {@code true} if case is ignored;  else {@code false}.
    */
   public boolean isCharType()
   {
      return ((flags & CHARACTER) != 0);
   }
   
   /**
    * Indicate if index component represents a character field and ignores case in data comparisons.
    *
    * @return  {@code true} if case is ignored;  else {@code false}.
    */
   public boolean isIgnoreCase()
   {
      return ((flags & IGNORE_CASE) != 0);
   }
   
   /**
    * Indicate if index component supports Progress semantic of using
    * abbreviated data for a sort or selection match.
    *
    * @return  {@code true} if abbreviated match is supported, else {@code false}.
    */
   public boolean isAbbreviated()
   {
      return ((flags & ABBREVIATED) != 0);
   }
   
   /**
    * Indicate if index component is mandatory.
    *
    * @return  {@code true} if mandatory, else {@code false}.
    */
   public boolean isMandatory()
   {
      return ((flags & MANDATORY) != 0);
   }
   
   /**
    * Compare this object to another {@code P2JIndexComponent}. It is assumed both objects contains data in
    * same space names.
    *
    * @param   other
    *          The {@code P2JIndexComponent} object to compare to.
    * @param   direction
    *          If the direction should also be tested. If {@code false} the direction is omitted.
    *
    * @return  {@code true} only if the two components represents the same field/property/column.
    */
   public boolean sameAs(P2JIndexComponent other, boolean direction)
   {
      if (normalizedName != null && !normalizedName.equals(other.normalizedName))
      {
         return false;
      }
      
      if (columnName != null && !columnName.equals(other.columnName))
      {
         return false;
      }
      
      if (propertyName != null && !propertyName.equals(other.propertyName))
      {
         return false;
      }
      
      // compare the direction is requested too 
      return !direction || isDescending() == other.isDescending();
   }
   
   /**
    * Test for equivalence;{@code o} is considered equivalent if it a {@code Component} instance with the same
    * name, sort direction, and abbreviation support as this component instance.
    *
    * @param   o
    *          An object instance, presumably a {@code Component} object, to test for equivalence with this
    *          object.
    *
    * @return  {@code true} if {@code o} is equivalent to this instance, else {@code false}.
    */
   @Override
   public boolean equals(Object o)
   {
      if (this == o)
      {
         return true;
      }
      if (o == null || getClass() != o.getClass())
      {
         return false;
      }
      
      P2JIndexComponent that = (P2JIndexComponent) o;
      
      if (flags != that.flags)
      {
         return false;
      }
      if (!Objects.equals(propertyName, that.propertyName))
      {
         return false;
      }
      if (!Objects.equals(columnName, that.columnName))
      {
         return false;
      }
      return Objects.equals(normalizedName, that.normalizedName);
   }
   
   /**
    * Hash code implementation which is consistent with this class'  implementation of {@link #equals}.
    *
    * @return  This object's precomputed hash code.
    */
   @Override
   public int hashCode()
   {
      return hash;
   }
   
   /**
    * Get a string representation of the internal state of this object, primarily for debugging purposes.
    *
    * @return  String representing this object's state.
    */
   @Override
   public String toString()
   {
      return "{" + legacyName + "/" + propertyName + "/" + columnName + ":" + flags + "}";
   }
   
   /**
    * Checks whether the requested name is {@code null}. An exception is thrown if the case.
    *
    * @param   name
    *          The name to be checked.
    * @param   space
    *          The namespace. Used for constructing the exception message.
    *
    * @throws  RuntimeException
    *          when the specified name is {@code null}. The message of the exception will contain a
    *          description of the internal structure to facilitate the dataflow and configured name-spaces.
    */
   private void checkNotNull(String name, String space)
   {
      if (name == null)
      {
         RuntimeException runtimeException = new RuntimeException(space + " == null for " + this);
         LOG.warning("", runtimeException);
         throw runtimeException;
      }
   }
}