raw.java

/*
** Module   : raw.java
** Abstract : Progress 4GL compatible raw data type
**
** Copyright (c) 2005-2023, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- ----------------------------Description-----------------------------
** 001 GES 20050906   @22563 Created initial version with support for a subset of raw processing.
** 002 ECF 20060403   @25321 Added copy constructor.
** 003 GES 20060724   @28173 Use forced assign in copy constructor.
** 004 GES 20090422   @41913 Converted to standard string formatting.
** 005 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.
** 006 GES 20130322          Fixed the BDT c'tor.
** 007 OM  20130329          Added instantiateUnknownRaw() static method.
** 008 GES 20130523          Major rewrite to implement a nearly complete implementation. Many
**                           deviations from the 4GL were found and fixed. The actual data access
**                           and storage is now in this class instead of in the parent, and this
**                           is exposed via a set of abstract worker methods that we implement
**                           here.
** 009 GES 20131212          Made isUninitialized() public.
** 010 MAG 20140602          Fixed the BDT c'tor and modified the assign(BDT, boolean) on _POLY
**                           morphing. Add compareTo(Object).
** 011 SVL 20140709          Removed debug output.
** 012 CA  20140811          Fixed clearing of unknown flag in assignment/output param.
** 013 ECF 20150715          Replace StringBuffer with StringBuilder.
** 014 OM  20160711          Update to match the BinaryData interface related to buffer length.
** 015 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.
** 016 OM  20160912          Small optimisations. Javadoc updates.
** 017 GES 20171207          Removed forced version of assign().
** 018 CA  20181128          Fixed return type of instantiateUnknownRaw.
** 019 IAS 20190617          Changed the writeByteRange method
** 020 CA  20190728          Fixed writeByteRange.
** 021 OM  20200507          Handling empty strings in constructor as a unknown value.
** 022 IAS 20200908          Rework (de)serialization.
** 023 IAS 20201007          Added Type enum
** 024 ME  20210504          Handle character assign, check dynamic data type conversion allowed.  
**     CA  20210818          Fixed undoable support - the changed value was being saved, instead of the 
**                           'before' byte array.
**     AL2 20220319          Added value proxy check in assign.
** 025 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 static com.goldencode.util.NativeTypeSerializer.*; 
import java.io.*;
import java.util.*;

import org.apache.commons.codec.binary.Base64;

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

/**
 * A class that implements common services for a Progress 4GL compatible raw data type whose data
 * is mutable.  Only a subset of the Progress language features are supported.
 * <p>
 * Format string related processing is handled in method {@link #toString(String)}.
 * <p>
 * All {@code RAW} features other than string formatting are implemented in the common base class
 * {@link BinaryData} which is shared with {@code MEMPTR} types.
 *<p>
 * In Progress 4GL, the actual maximum size of a {@code RAW} cannot be reliably
 * determined.  The largest value that has been found to consistently work is 31990. But
 * supposedly up to 32000 should work.  It turns out these don't work in practice. This
 * implementation works to the 32000 size. Although the documentation suggests that 32kb
 * (32768 bytes or 32 times 1024) is the limit, attempting to use values of that size
 * actually cause memory access violations and abends, regardless of what the 4GL
 * documentation states. This class does NOT duplicate the instability or 4GL process 
 * abends, but is otherwise compatible. 
 */
public class raw
extends BinaryData
{
   /** The maximum size of the binary data stored in an instance. */ 
   public static final int MAX_SIZE = 32000;
   
   /**
    * Stores the state of this instance as an array of bytes.  This is independent of the state
    * of whether or not this instance represents {@code unknown}.
    */
   private byte[] value = null; 
   
   /**
    * Default constructor, creates an instance that represents a 0 length byte array.
    */
   public raw()
   {
      value = new byte[0];
   }

   /**
    * 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 raw(BaseDataType value)
   {
      // the value may be a proxy; make sure to unwrap and work with the real value
      value = value == null ? null : value.val();
      if (value == null || value.isUnknown())
      {
         setUnknown();
      }
      else
      {
         if (value instanceof Text)
         {
            // FIXME Is any valid value? 
            // Why P4G show this message for character and longchar?
            String err = "Invalid encoded representation of RAW data.";
            ErrorManager.recordOrThrowError(4957, err);      
         }
         else
         {
            assign(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 {@link Comparable} interface.
    * <p>
    * The algorithm will fail to give meaningful results in the case where
    * one tries to sort against other objects that do not represent compatible values.
    *
    * @param   obj
    *          The instance to compare against.
    *
    * @return  -1, 0 or 1 depending on if this instance is less than, equal to or greater
    *          (respectively) than the specified instance {@code obj}.
    */
   @Override
   public int compareTo(Object obj)
   {
      if (obj instanceof BaseDataType)
      {
         return super.compareTo(new raw((BaseDataType) obj));
      }
      
      return super.compareTo(obj);
   }
   
   /**
    * Constructs an instance after copying the parameter's data into the
    * internal representation of this class.
    * <p>
    * If the parameter's represents {@code unknown}, this instance will also represent
    * {@code unknown}.
    *
    * @param   value
    *          The value to be used for this instance.
    */
   public raw(raw value)
   {
      assign(value);
   }
   
   /**
    * Constructor which creates an instance which is initialized based on the given binary data.
    *
    * @param    val
    *           The binary data to be used as the contents or if {@code null} then the
    *           instance will represent {@code unknown}.
    */
   public raw(byte[] val)
   {
      super(val);
      replaceContents(Arrays.copyOf(val, val.length));
   }
   
   /**
    * Constructs an instance which converts the contents of the given
    * Progress 4GL compatible string form of a raw value into the proper
    * binary form.  When Progress converts a raw value into a string,  
    * it inserts 6 hexadecimal characters at the front of the string and
    * then formats all other data as a single Base64 encoded value (it
    * is NOT split into 76 character line segments).  The first 2 hexadecimal
    * characters are always {@code 02} and the following 4 hexadecimal
    * characters are a hexadecimal representation of the length of the
    * resulting value.  These 6 preceding bytes are simply dropped.
    *
    * @param    value
    *           The Base64 value to be converted into the resulting
    *           instance binary data.
    */
   public raw(String value)
   {
      if (value == null)
      {
         setUnknown();
      }
      else if (value.isEmpty())
      {
         this.value = new byte[0]; // same as the default c'tor
      }
      else
      {
         replaceContents(Base64.decodeBase64(value.substring(6).getBytes()));
      }
   }
   
   /**
    * Constructs an instance which converts the contents of the given
    * Progress 4GL compatible string form of a raw value into the proper
    * binary form.  When Progress converts a raw value into a string,  
    * it inserts 6 hexadecimal characters at the front of the string and
    * then formats all other data as a single Base64 encoded value (it
    * is NOT split into 76 character line segments).  The first 2 hexadecimal
    * characters are always {@code 02} and the following 4 hexadecimal
    * characters are a hexadecimal representation of the length of the
    * resulting value.  These 6 preceding bytes are simply dropped.
    *
    * @param    value
    *           The Base64 value to be converted into the resulting instance binary data. If this
    *           is the {@code unknown}, then the result is the {@code unknown}.
    */
   public raw(character value)
   {
      if (value == null || value.isUnknown())
      {
         setUnknown();
      }
      else
      {
         String str = value.getValue().substring(6);
         replaceContents(Base64.decodeBase64(str.getBytes()));
      }
   }
   
   /**
    * Creates a new instance of the same type that represents {@code unknown}.
    *
    * @return   An instance that represents {@code unknown}.
    */
   public static raw instantiateUnknownRaw()
   {
      raw r = new raw();
      r.setUnknown();
      
      return r;
   }

   /**
    * Get the type
    * @return type
    */
   public Type getType()
   {
      return Type.RAW;
   }

   /**
    * Does the same as standard {@link #clone()} method but returns an instance of
    * {@link BaseDataType} and doesn't throw the {@link CloneNotSupportedException}.
    *
    * @return   A clone of this instance.
    */
   public raw duplicate()
   {
      raw r = new raw();
      
      // set the instance data
      r.assign(this);
      
      return r;
   }

   /**
    * Creates a new instance of the same type that represents {@code unknown}.
    *
    * @return   An instance that represents {@code unknown}.
    */
   public BaseDataType instantiateUnknown()   
   {
      return raw.instantiateUnknownRaw();
   }
   
   /**
    * 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 raw();
   }

   /**
    * Reports if this instance represents an uninitialized buffer. Being uninitialized is not the
    * same as being of zero length.  Rather it means that the buffer was never allocated. Being
    * {@code unknown} is also different from being uninitialized.
    *
    * @return    {@code true} if this instance is uninitialized.
    */
   public boolean isUninitialized()
   {
      return (value == null);
   }
   
   /**
    * Creates a default string representation of the instance data which will be equivalent to the
    * result of {@link #toStringExport}.  If the instance represents {@code unknown}, a
    * {@code '?'} will be returned.
    *
    * @return   The formatted string.
    */
   public String toString()   
   {
      return toString(null);
   }
   
   /**
    * Creates a string representation of the instance data using the given format string.
    * A {@code null} format string is equivalent to the result of {@link #toStringExport}.
    * If the instance is {@code unknown}, a '?' will be returned.
    *
    * @param    fmt
    *           The format string to use.
    *
    * @return   The formatted string.
    */
   public String toString(String fmt)
   {
      if (fmt == null)
      {
         return toStringExport();
      }
      
      if (unknown)
      {
         return "?";
      }
      
      if (fmt.length() == 0)
      {
         ErrorManager.recordOrThrowError(74, "Value  cannot be displayed using ");
         return "";
      }
      
      // use the normal character type to handle this data
      character tmp = new character(new String(value));
      
      return tmp.toString(fmt);
   }
   
   /**
    * Creates a string representation of the instance data in a form that is compatible with the
    * {@code MESSAGE} language statement.  If the instance represents {@code unknown}, a 
    * {@code '?'} will be returned.
    *
    * @return   The 'message' formatted string.
    */
   public String toStringMessage()   
   {
      return toStringExport();
   }
   
   /**
    * Creates a string representation of the instance data using the 'export'
    * format in which the data is written as a base64 string prepended with 6
    * hexadecimal bytes of "header" information. Warning: this method is 
    * hard-coded to produce a header string starting with "02" (this seems
    * to be a fixed value for normal {@code raw} data in Progress, but
    * it may be different when records or other structured data is written)
    * and this is followed by 4 bytes specifying the length of the data.
    * <p>
    * If the instance represents {@code unknown}, a '?' will be returned.
    * 
    * @return   The 'export' formatted string.
    */
   public String toStringExport()   
   {
      // quick out
      if (unknown)
      {
         return "?";
      }
      
      // always starts with 0x02
      StringBuilder sb = new StringBuilder("02");
      
      // add length as 4 hexadecimal chars
      sb.append(String.format("%04X", value.length));
      
      if (value.length > 0)
      {
         // add the contents of the array encoded base64
         sb.append(new String(Base64.encodeBase64(value)));
      }
      
      return sb.toString();
   }
   
   /**
    * Sets the array to a given length, which may result in either extending
    * or truncating the array.
    *
    * @param    len
    *           New length of the array.
    *
    * @return   The current instance is returned.
    */
   public BinaryData setLength(long len)
   {
      extendBytes(len);
      return this;
   }
   
   /**
    * 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.
    */
   public void readExternal(ObjectInput in)
   throws IOException,
          ClassNotFoundException
   {
      super.readExternal(in);
      value = readByteArray(in);
   }

   /**
    * 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.
    */
   public void writeExternal(ObjectOutput out)
   throws IOException
   {
      super.writeExternal(out);
      writeByteArray(out, value);
   }
   
   /**
    * Sets the state of this instance's {@code unknown} flag to {@code true}.
    * <p>
    * <b>Warning: the data stored in this instance WILL BE deallocated after calling this
    * method.</b>
    */
   public void setUnknown()
   {
      if (!unknown)
      {
         checkUndoable(true);
      }
      
      super.setUnknown();
   }

   /**
    * Sets the state of this instance's {@code unknown} flag to {@code false}.
    */
   @Override
   protected void clearUnknown()
   {
      if (unknown)
      {
         checkUndoable(true);
      }

      super.clearUnknown();
   }

   /**
    * Release any resources associated with the instance.
    */
   protected void deallocate()
   {
      checkUndoable(() -> new raw(new byte[0]));
      
      value = new byte[0];
   }
   
   /**
    * Determines if the array of binary data stored in an instance of this
    * class must be limited in length.
    *
    * @return    {@code true} if the length must be limited.
    */
   protected boolean isLengthLimited()
   {
      return true;
   }
   
   /**
    * Determines the maximum length of the array of binary data stored in an
    * instance of this class.
    *
    * @return    Always MAX_SIZE (32KB).
    */
   protected long lengthLimit()
   {
      return MAX_SIZE;
   }
   
   /**
    * Determines if the array of binary data stored in an instance of this
    * class must be automatically extended in length when operations
    * write past the current length.  Such extensions will pad with
    * {@code null} bytes.
    *
    * @return    {@code true} if the length must automatically extended.
    */
   protected boolean isAutoExtend()
   {
      return true;
   }
   
   /**
    * Replace the entire contents of the current buffer with the given array.
    * <p>
    * The {@link BinaryData#unknown} flag is also cleared, only if {@code value} is not 
    * {@code null} and if the length is greater than 0.
    * 
    * @param    value
    *           The new contents.
    */
   protected void replaceContents(byte[] value)
   {
      checkUndoable(() -> new raw(value));
      
      this.value = value;

      if (value != null && value.length > 0)
      {
         clearUnknown();
      }
   }
   
   /**
    * Return the entire contents of the current buffer as an array. This may be a copy
    * of the data, depending on the subclass' implementation.  DO NOT MODIFY the data
    * that is returned, since it is undefined as to whether the changes will or will not
    * be reflected in the actual storage.
    *
    * @return    The contents of the instance (possibly a copy). If the instance is uninitialized
    *            the returned value will be a 0 length array. If the instance is {@code unknown}
    *            or has an undefined size, then {@code null} will be returned.
    */
   protected byte[] asByteArray()
   {
      return unknown ? null : value;
   }
   
   /**
    * Reports the current length of the buffer.
    *
    * @return  The length or 0 if uninitialized.
    */
   @Override
   protected long visibleLength()
   {
      return internalLength();
   }
   
   /**
    * Reports the current length of the buffer.
    *
    * @return  The length or 0 if uninitialized.
    */
   @Override
   protected long internalLength()
   {
      return (value == null) ? 0 : value.length;
   }
   
   /**
    * Returns the next index position into the array which is the {@code null} byte,
    * starting at a given index position.
    *
    * @param    pos
    *           The 0-based index into the array at which to start searching.
    *
    * @return   The next index into the array which is the {@code null} byte or the length
    *           of the array if no {@code null} byte exists.
    */
   protected long findNextNull(long pos)
   {
      // arrays can only have 32-bit indexes in the Java Language Spec
      int i = (int) pos;
      
      // not all instances will end in null byte, so check length first to prevent overrun
      while (i < value.length && value[i] != '\0')
      {
         i++;
      }
      
      return i;
   }

   /**
    * Helper to create a new byte array with the same contents as the given byte array and the
    * specified length.  If the given target length is smaller than the source array length, the
    * output array is truncated to the specified size.  If the target length is larger than the
    * source array length, the output array is extended to the given size and any additional
    * bytes are initialized to '\0'.
    * <p>
    * This is safe to use on 0 length arrays.
    * <p>
    * If the instance is {@code unknown}, this silently returns without making any changes.
    *
    * @param    len
    *           The output array length.  Negative values are silently ignored.  Positive values
    *           up to {@link #MAX_SIZE} are accepted. Values greater than
    *           {@link #MAX_SIZE} will cause an error condition to be raised.
    */
   protected void extendBytes(long len)
   {
      if (unknown || len < 0)
      {
         return;
      }
      
      if (len > MAX_SIZE)
      {
         genLimitedLengthError();
         return;
      }
      
      byte[] newVal;
      
      if (value == null)
      {
         newVal = new byte[(int) len];
      }
      else
      {
         newVal = Arrays.copyOf(value, (int) len);
      }
      
      checkUndoable(() -> new raw(newVal));
      
      value = newVal;
   }
   
   /**
    * Read the byte at the given location.  All boundary checking and error handling must have
    * already been done in the caller such that this is known to be a valid access. Any
    * unexpected failure may cause a platform-specific exception that terminates the process!
    *
    * @param    pos
    *           The 0-based index position at which to read the byte.
    *
    * @return   The byte at that location.
    */
   protected byte readByte(long pos)
   {
      return value[(int) pos];
   }
   
   /**
    * Write the byte into the given location. All boundary checking and error handling must have
    * already been done in the caller such that this is known to be a valid access. Any
    * unexpected failure may cause a platform-specific exception that terminates the process!
    *
    * @param    val
    *           The byte to write.
    * @param    pos
    *           The 0-based index position at which to read the byte.
    */
   protected void writeByte(byte val, long pos)
   {
      if (value[(int) pos] != val)
      {
         checkUndoable(true);
      }

      value[(int) pos] = val;
   }
   
   /**
    * Read the bytes at the given location.  All boundary checking and error handling must have
    * already been done in the caller such that this is known to be a valid access. Any
    * unexpected failure may cause a platform-specific exception that terminates the process!
    *
    * @param    pos
    *           The 0-based index position at which to read the bytes.
    * @param    len
    *           The number of bytes to read.
    * @param    endian
    *           {@code true} to force the returned data to be interpreted by the endian-ness
    *           of the native platform.  The returned data will be in little endian order if this
    *           flag is set (but on a big endian platform the bytes will be reversed first).
    *
    * @return   The byte range at that location.
    */
   protected byte[] readByteRange(long pos, long len, boolean endian)
   {
      // create buffer to contain the data
      byte[] data = new byte[(int) len];
      
      // find the ending index; array indexes are limited to 32-bit in the Java Language Spec
      int idx = (int) pos;
      int end = idx + (int) len;
      
      // copy the data in (may copy nothing if end == pos)
      System.arraycopy(value, idx, data, 0, end - idx);
      
      // we always return data in little endian order when the endian flag is set
      if (endian && !memptr.isLittleEndianPlatform(null))
      {
         data = reverseBytes(data);
      }
      
      return data;
   }
   
   /**
    * Write the bytes at the given location.  All boundary checking and error handling must have
    * already been done in the caller such that this is known to be a valid access. Any
    * unexpected failure may cause a platform-specific exception that terminates the process!
    *
    * @param    data
    *           The bytes to write.
    * @param    pos
    *           The 0-based index position at which to start writing the bytes.
    * @param    len
    *           How many bytes should be written. -1 means write everything to the end of the
    *           array.
    * @param    endian
    *           {@code true} to honor the endian-ness of the platform. The {@code data}
    *           array MUST be ordered in little-endian order if the endian flag is on. 
    */
   protected void writeByteRange(byte[] data, long pos, long len, boolean endian)
   {
      // the data is always passed into us in little endian order when the endian flag is set
      if (endian && !memptr.isLittleEndianPlatform(null))
      {
         data = reverseBytes(data);
      }
      
      // find the ending index; array indexes are limited to 32-bit in the Java Language Spec
      int idx = (int) pos;
      int end = Math.min(idx + (int)(len == -1 ? data.length : len), value.length);
      
      boolean changed = false;
      
      // check the data if has changed
      for (int i = idx; i < end; i++)
      {
         byte newVal = data[i - idx];

         if (!changed && value[i] != newVal)
         {
            changed = true;
            break;
         }
      }
      
      if (changed)
      {
         checkUndoable(true);
      }
      
      // copy the data in
      for (int i = idx; i < end; i++)
      {
         value[i] = data[i - idx];
      }
   }   
   
   
   @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();
      }
      
      // unknown or empty string assignments are not performed
      if (value instanceof character && TextOps.isEmpty((character) value))
         return true;
      
      return super.isIncompatibleTypesOnConversion(value);
   }

   @Override
   public void assign(BaseDataType data)
   {
      if (data != null)
      {
         data = data.val();
      }
      
      if (data instanceof character && !data.isUnknown())
      {
         // first two characters should be "02"
         // following 4 are length of the initial raw "encoded" as string (hex)
         // the rest is base64 encoded value of initial raw
         String value = ((character) data).toStringMessage();
         
         if (value.startsWith("02") && value.length() >= 6) 
         {
            assign(Base64.decodeBase64(value.substring(6).getBytes()));
         }
         else
         {
            ErrorManager.recordOrThrowError(4957, "Invalid encoded representation of RAW data");
         }
      }
      else
      {
         super.assign(data);
      }
   }
}