rowid.java

/*
** Module   : rowid.java
** Abstract : Progress 4GL compatible rowid object
**
** Copyright (c) 2005-2023, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- ------------------Description-------------------
** 001 ECF 20080721   @39290 Created initial version. Represents the
**                           Progress rowid data type. Much of the
**                           implementation was borrowed from the handle
**                           type.
** 002 ECF 20090212   @41302 Implemented a faster equals(). Handles most
**                           common cases without having to invoke the
**                           superclass' implementation.
** 003 CA  20130123          Added c'tor which builds a new instance from a
**                           BaseDataType value, if possible. This should be
**                           used only when constructing an instance using the
**                           value returned by a DYNAMIC-FUNCTION call; 4GL
**                           does some special casting in this case, which is
**                           not implemented yet.
** 004 OM  20121023          Added/fixed support for TO-ROWID(STRING(ROWID)).
** 005 CS  20130315          Made parseRowidString public.
** 006 GES 20130322          Fixed the BDT c'tor and modified the assign(BDT,boolean) to be the
**                           common location for the _POLY morphing. This is safe because all
**                           current usage would have generated a ClassCastException if that
**                           method was ever used with an invalid type. Now, instead of the CCE,
**                           the code will morph the value as is possible. The morphing code
**                           still needs to be implemented.
** 007 OM  20130416          Cosmetic change: removed '.' at the end of error message.
** 008 MAG 20140602          Fixed the BDT c'tor and modified the assign(BDT,boolean) on _POLY morphing.
**                           Add BDT types on compareTo(Object).
** 009 ECF 20151209          Removed bitwise parity check and converted characters to lowercase
**                           when parsing rowid strings.
** 010 EVL 20160224          Javadoc fixes to make compatible with Oracle Java 8 for Solaris 10.
** 011 GES 20160617          Added unknown literal processing for the polymorphic assign case.
** 012 CA  20160627          Reworked undoable support - the undoables register themselves with
**                           all blocks up the stack, until either 1. the tx block which created
**                           it is reached or 2. the last tx block where it was saved is reached.
** 013 GES 20171206          Removed explicit bypass on pending error (new silent error mode).
** 014 GES 20180420          Implemented the backing support for serialization because it is
**                           needed for RAW-TRANSFER.
** 015 OM  20190206          Added implementation of field size needed by record-length.
** 016 IAS 20201007          Added Type enum
** 017 VVT 20210331          parseRowidString(String) fixed: does not throw anymore;
**                           toString(String fmt) fixed: now always emit all 16 digits. See #5218.
**     ME  20210504          Added character/longchar value assign (poly conversion).
**     CA  20210609          Reworked INPUT/INPUT-OUTPUT parameters to a new approach, where they are explicitly 
**                           initialized at the method's execution, and not at the caller's arguments.
**     AL2 20220319          Added value proxy check in assign.
** 018 CA  20230215          'instantiateDefault' now is implemented by each sub-class.
**     CA  20230215          'duplicate()' method returns the real type instead of BDT.
*/

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

import java.io.*;
import java.util.regex.*;

/**
 * A class that represents a Progress 4GL compatible rowid data type.
 * Internally, the row identifier is stored as a 64-bit integer.
 */
public class rowid
extends BaseDataType
{
   /** Stores the integral row identifier, unique within a database */
   private Long value = null;
   
   /** Rowid valid string representation regex pattern. */ 
   private final static Pattern RowIdPattern = Pattern.compile("0x[0]*([0-9a-f]{1,})");
   
   /**
    * Worker method that parses and validates a <code>java.lang.String</code>
    * according to rowid Progress data type constraints:
    * <ul>
    *    <li>must start with "0x" prefix
    *    <li>must only contain lower-case hexadecimal digits
    *    <li>the number of digits must be more than 2 and even
    *    <li>the parsed value must fit into a 64-bit unsigned integer (this is the FWD limitation,
    *        Progress accepts any number of digits.)
    * </ul>
    *
    * @param   value
    *          A hex string matching the conditions above.
    *          Any other string, including <code>null</code>, will
    *          indicate the unknown value.
    *
    * @return  A <code>Long</code> representing the internal value for a
    *          rowid or <code>null</code> if parsing fails.
    */
   public static Long parseRowidString(String value)
   {
      // 4gl requires the total number of digits be even
      if (value == null || (value.length() % 2 == 1))
      {
         return null;
      }

      final Matcher matcher = RowIdPattern.matcher(value);
      if (!matcher.matches())
      {
         return null;
      }

      // Get the [start,end) of actual digits without the leading zeroes
      // (which were skipped by the pattern).
      final int start = matcher.start(1);
      final int end = matcher.end(1);

      // Assert we will not get integer overflow
      if ((end - start) > 16)
      {
         return null;
      }

      // Parse the number
      long result = 0;
      for (int i = start; i < end; i++)
      {
         final char c = value.charAt(i);
         result = result * 16 + (c <= '9' ? c - '0' : (c - 'a' + 10));
      }

      return Long.valueOf(result);
   }
   
   /**
    * Default constructor, creates an instance that represents the unknown
    * value.
    */
   public rowid()
   {
   }
   
   /**
    * This is a special c'tor which should be used only when converting the
    * value returned by a function or method with polymorphic return type into the
    * expected type (i.e. DYNAMIC-FUNCTION()).  In such cases, the 4GL does some
    * automatic type conversion (see {@link #assign(BaseDataType)}).
    * 
    * @param    value
    *           The value to be used for this instance.
    */
   public rowid(BaseDataType value)
   {
      initialize(value);
   }
   
   /**
    * Constructs an instance based upon an existing instance.
    *
    * @param   value
    *          The instance to copy.
    */
   public rowid(rowid value)
   {
      assign(value);
   }
   
   /**
    * Constructs an instance given a 64-bit integer value.
    * 
    * @param   value
    *          64-bit integer value;  should be unique across all records in a
    *          database.  May be <code>null</code> to indicate unknown value.
    */
   public rowid(Long value)
   {
      this.value = value;
   }
   
   /**
    * Constructs an instance given a character encoding (hex string in the
    * format 0xhhhhhhhhhhhhhhhh, where each h is an lower-case, hexadecimal 
    * digit, limited to maximum 16 digits because that actual internal 
    * implementation uses a java 64-bit Long).
    *
    * @param   val
    *          A hex string in the format 0xhhhhhhhhhhhhhhhh.
    *          Any other string will, including <code>null</code> will
    *          indicate the unknown value.
    */
   public rowid(character val)
   {
      // the value may be a proxy; make sure to unwrap and work with the real value
      if (BaseDataType.isProxy(val))
      {
         if (val.val() instanceof character)
         {
            val = (character) val.val();
         }
         else
         {
            initialize(val);
            return;
         }
      }
      
      // trivial cases:
      if (val == null || val.isUnknown())
      {
         this.value = null;
         return;
      }
      
      this.value = parseRowidString(val.toJavaType());
   }
   
   /**
    * Constructs an instance given a character encoding (hex string in the
    * format 0xhhhhhhhhhhhhhhhh, where each h is an lower-case, hexadecimal 
    * digit, limited to maximum 16 digits because that actual internal 
    * implementation uses a java 64-bit Long).
    *
    * @param   val
    *          A hex string in the format 0xhhhhhhhhhhhhhhhh.
    *          Any other string will, including <code>null</code> will
    *          indicate the unknown value.
    */
   public rowid(String val)
   {
      this.value = parseRowidString(val);
   }
   
   /**
    * Creates a new instance of the same type that represents the 
    * <code>unknown value</code>.
    *
    * @return  An instance that represents the <code>unknown value</code>.
    */
   public BaseDataType instantiateUnknown()
   {
      return new rowid();
   }
   
   /**
    * Creates a new instance of the same type that represents the default initialized value.
    *
    * @return   An instance that represents the default value.
    */
   @Override
   public BaseDataType instantiateDefault()
   {
      return new rowid();
   }

   /**
    * Does the same as standard <code>clone()</code> method but returns an
    * instance of <code>BaseDataType</code> and doesn't throw the
    * <code>CloneNotSupportedException</code>.
    *
    * @return  A clone of this instance.
    */
   public rowid duplicate()
   {
      return (new rowid(this));
   }
   
   /**
    * Get the type
    * @return type
    */
   public Type getType()
   {
      return Type.ROWID;
   }

   /**
    * An equality test which handles the most common cases with the least
    * amount of overhead.  The superclass implementation is invoked otherwise.
    * <p>
    * This implementation is consistent with {@link #hashCode()}.
    * 
    * @see com.goldencode.p2j.util.BaseDataType#equals(java.lang.Object)
    */
   public boolean equals(Object o)
   {
      if (this == o)
      {
         return true;
      }
      
      if (o instanceof rowid)
      {
         rowid that = (rowid) o;
         
         boolean u1 = (this.value == null);
         boolean u2 = (that.value == null);
         
         if (u1 && u2)
         {
            return true;
         }
         
         if (u1 || u2)
         {
            return false;
         }
         
         return (this.value.equals(that.value));
      }
      
      return super.equals(o);
   }
   
   /**
    * Hash code implementation which is consistent with {@link
    * BaseDataType#equals}.
    *
    * @return  Hash code value for this object instance. All instances of
    *          this class return the same hash code value, since all
    *          instances are inherently equal.
    */
   public int hashCode()
   {
      int result = 17;
      
      if (value != null)
      {
         result = 37 * result + value.hashCode();
      }
      
      return result;
   }
   
   /**
    * Returns the value of this instance as an <code>Long</code>.
    * <p>
    * If this instance represents the <code>unknown value</code>, a
    * <code>null</code> will be returned.
    *
    * @return  The value as a <code>Long</code> or <code>null</code> if
    *          this is <code>unknown</code>.
    */
   public Long getValue()
   {
      return value;
   }
   
   /**
    * Determines if this instance represents the <code>unknown value</code>.
    *
    * @return  <code>true</code> if this instance is set to the
    *          <code>unknown value</code>.
    */
   public boolean isUnknown()
   {
      return (value == null);
   }
   
   /**
    * Sets this instance to represent the unknown value.
    * <p>
    * Any existing value is discarded.
    */
   public void setUnknown()
   {
      if (value != null)
      {
         checkUndoable(true);
      }
      
      value = null;
   }
   
   /**
    * Sets the data of this instance based on the given instance.
    * <p>
    * If the value is not of type <code>rowid</code>, the following automatic type
    * conversion will occur:
    * <p>
    * <pre>
    * This type support only rowid and unknown types (including unknown literal).
    * </pre>
    * <p>
    * This variant is meant to handle the cases of built-in functions and methods in the 4GL
    * which have polymorphic return types (e.g. DYNAMIC-FUNCTION()). This should NOT be used for
    * non-polymorphic assignments.
    *
    * @param   value
    *          The instance from which to copy state.
    * 
    * @throws  ClassCastException
    *          if <code>value</code> is not an instance of this class.
    */
   public void assign(BaseDataType value)
   {
      if (value == null)
      {
         setUnknown();
         return;
      }
      
      // the value may be a proxy; make sure to unwrap and work with the real value
      value = value.val();
      if (value.isUnknown())
      {
         setUnknown();
      }
      else if (value instanceof rowid)
      {
         assign((rowid) value);
      }
      else if (value instanceof Text)
      {
         assign((Text) value);
      }
      else
      {
         // conversion impossible
         incompatibleTypesOnConversion();
      }
   }
   
   /**
    * Sets the data of this instance based on the given instance.
    *
    * @param   value
    *          The instance from which to copy state.
    */
   public void assign(rowid value)
   {
      // the value may be a proxy; make sure to unwrap and work with the real value
      if (BaseDataType.isProxy(value))
      {
         assign(value.val());
         return;
      }
      checkUndoable(value);

      this.value = value.value;
   }
   
   /**
    * Sets the data of this instance based on the given instance.
    *
    * @param   value
    *          The instance from which to copy state.
    */
   public void assign(Text value)
   {
      checkUndoable(value);

      this.value = parseRowidString(value.toJavaType());
   }
   
   /**
    * Sets the state (data and unknown value) of this instance based on the
    * state of the passed instance.
    *
    * @param   value
    *          The instance from which to copy state.
    */
   public void assign(Undoable value)
   {
      assign((rowid) value);
   }
   
   /**
    * Compares this instance with the specified instance and returns a -1
    * if this instance is less than the specified, 0 if the two instances
    * are equal and 1 if this instance is greater than the specified
    * instance.  This is the implementation of the <code>Comparable</code>
    * interface.
    * <p>
    * The rowids' internal, 64-bit integer values are compared.
    *
    * @param   obj
    *          The instance to compare against.
    *
    * @return   0 or 1 depending on if the passed instance is or is not
    *           <code>unknown</code>.
    */
   public int compareTo(Object obj)
   {
      rowid that = null;
      
      if (obj instanceof rowid)
      {
         that = (rowid) obj;
      }
      else if (obj instanceof BaseDataType)
      {
         that = new rowid((BaseDataType) obj);
      }
      else
      {
         return -1;
      }
      
      Long l1 = this.value;
      Long l2 = that.value;
      
      if (l1 == l2)
      {
         return 0;
      }
      else if (l1 == null)
      {
         return 1;
      }
      else if (l2 == null)
      {
         return -1;
      }
      
      return l1.compareTo(l2);
   }
   
   /**
    * Creates a string representation of the instance data as a hex string
    * with a leading "0x" prefix.
    * <p>
    * If the instance represents the <code>unknown value</code>, a '?' will be
    * returned.
    *
    * @return  The formatted string.
    */
   public String toString()
   {
      return toString(null);
   }
   
   /**
    * Creates a string representation of the instance data as a hex string
    * with a leading "0x" prefix.
    * <p>
    * If the instance represents the <code>unknown value</code>, a '?' will be
    * returned.
    *
    * @param   fmt
    *          The Progress 4GL format string or <code>null</code> if the
    *          default format is to be used.  This parameter is not validated and
    *          is ignored.
    *
    * @return  The formatted string.
    */
   public String toString(String fmt)
   {
      if (isUnknown())
      {
         return "?";
      }
      
      return String.format("0x%016x", value);
   }
   
   /**
    * Creates a string representation of the instance data in a form that is
    * compatible with the <code>MESSAGE</code> language statement.  If the
    * instance represents the <code>unknown value</code>, a '?' will be
    * returned.
    *
    * @return  The 'message' formatted string.
    */
   public String toStringMessage()
   {
      return toString(null);
   }
   
   /**
    * Creates a string representation of the instance data using the 'export'
    * format.  If the instance represents the <code>unknown value</code>, a
    * '?' will be returned.
    *
    * @return  The 'export' formatted string.
    */
   public String toStringExport()
   {
      return toString(null);
   }
   
   /**
    * Return the default display format string for this type.  This
    * implementation returns <code>null</code>, because there is no formatted
    * representation of a rowid:  it must be converted to a string first using
    * the Progress STRING() function.
    *
    * @return  <code>null</code>.
    */
   public String defaultFormatString()
   {
      return null;
   }
   
   /**
    * Calculate the length of a formatted value (the text form of the value)
    * using this format string.
    *
    * @param    fmt
    *           The format string.
    *
    * @return   The length in characters of the any resulting formatted
    *           value.
    */
   public int formatLength(String fmt)
   {
      return 18;  // an 8-byte hex string + leading '0x' (0x0123456789ABCDEF)
   }
   
   /**
    * Obtain the length (in bytes) of this BDT will use when serialized. 
    *
    * @return  the length of this BDT will use when serialized.
    */
   @Override
   public int getSize()
   {
      return isUnknown() ? 1 : 9;
   }
   
   /**
    * Replacement for the default object reading method. The latest
    * state is read from the input source.
    * 
    * @param   in
    *          The input source from which fields will be restored.
    *
    * @throws  IOException
    *          in case of I/O errors.
    * @throws  ClassNotFoundException
    *          if payload can't be instantiated.
    * @throws  NotSerializableException
    *          always.
    */
   public void readExternal(ObjectInput in)
   throws IOException,
          ClassNotFoundException
   {
      boolean unknown = in.readBoolean();
      
      if (!unknown)
      {
         value = in.readLong();
      }
   }
   
   /**
    * Replacement for the default object writing method. The latest
    * state is written to the output destination.
    * 
    * @param   out
    *          The output destination to which fields will be saved.
    *
    * @throws  IOException
    *          in case of I/O errors.
    * @throws  NotSerializableException
    *          always.
    */
   public void writeExternal(ObjectOutput out)
   throws IOException
   {
      boolean unknown = isUnknown();
      
      out.writeBoolean(unknown);
      
      if (!unknown)
      {
         out.writeLong(value);
      }
   }
   
   @Override
   protected boolean isIncompatibleTypesOnConversion(BaseDataType value)
   {
      // invalid conversion is being thrown even if unknown
      if (value instanceof clob || value instanceof blob || value instanceof longchar) 
      {
         incompatibleTypesOnConversion();
      }
      
      return super.isIncompatibleTypesOnConversion(value);
   }
}