# blob.java

    /*
    ** Module   : blob.java
    ** Abstract : Data wrapper for SQL BLOB data.
    **
    ** Copyright (c) 2018-2023, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
    ** 001 ECF 20181214 Created initial version.
    ** 002 CA  20190516 Added blob(BDT) constructor.
    ** 003 IAS 20190617 Changed the signature of the writeByteRange method
    ** 004 CA  20190628 Added write/readExternal.
    ** 005 ECF 20190628 Implemented COPY-LOB runtime.
    ** 006 SVL 20191002 Added assign(blob) in order to work around an issue when a dynamic record with
    **                  a blob field cannot be created.
    ** 007 CA  20191118 blob initial state is unknown.
    ** 008 CA  20191203 Fixed overlay and unknown COPY-LOB related bugs.
    ** 009 IAS 20200908 Rework (de)serialization.
    ** 010 IAS 20201007 Added Type enum
    **     OM  20210312 Added parent FieldReference accessors in case the LOB was obtained by dereferenciation
    **                  of a buffer field. Improved COPY-LOB engine and validations.
    ** 011 AL2 20220328 Added proxy checks for parameter BDT.
    **     EVL 20221222 Adding runtime support for LOB fields export.
    ** 012 CA  20230215 'instantiateDefault' now is implemented by each sub-class.
    **     CA  20230215 'duplicate()' method returns the real type instead of BDT.
    ** 013 IAS 20230225 Avoid NPE in <code>writeExternal</code>.
    */

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

    import com.goldencode.p2j.persist.*;

    /**
     * Data wrapper type for SQL BLOB data.
     */
    public class blob
    extends BinaryData
    implements LargeObject
    {
       /** Maximum size of the blob */
       private static final int MAX_SIZE = 1024 * 1024 * 1024;

       /** Backing data */
       private byte[] value = null;

       /** SQL BLOB object */
       private Blob sqlBlob = null;

       /** The parent buffer field where this object was obtained by dereferenciation, if this is the case. */
       private FieldReference fRef;

       /** The field name used with current field reference. */
       private String fieldName = null;

       /**
        * Default constructor.
        */
       public blob()
       {
          setUnknown();
       }

       /**
        * Constructor used for creating initial field from default {@code String}.
        * @param def
        *        default value
        */
       public blob(String def)
       {
          assign((byte[]) null);
       }

       /**
        * Copy constructor.
        *
        * @param   value
        *          Blob to be copied.
        */
       public blob(blob value)
       {
          if (value == null)
          {
             assign((byte[]) null);
          }
          else
          {
             // the value may be a proxy; make sure to unwrap and work with the real value
             if (BaseDataType.isProxy(value))
             {
                if (value.val() instanceof blob)
                {
                   value = (blob) value.val();
                }
                else
                {
                   assign(value.val());
                   return;
                }
             }
             assign(value.asByteArray());
             this.sqlBlob = value.sqlBlob;
          }
       }

       /**
        * Constructor which is initialized from a database record.
        *
        * @param   sqlBlob
        *          SQL BLOB from which data is read.
        *
        * @throws  SQLException
        *          if there is an error reading data.
        */
       public blob(Blob sqlBlob)
       throws SQLException
       {
          this.sqlBlob = sqlBlob;

          // TODO: enable optimization to skip this step in certain cases (e.g., query field list
          // exclusion)
          readData();
       }

       /**
        * Constructor which initializes an instance from the given data.
        *
        * @param   value
        *          Data.
        */
       public blob(byte[] value)
       {
          assign(value);
       }

       /**
        * Constructor which initializes an instance from a memptr object.
        *
        * @param   value
        *          Pointer from which data is copied.
        */
       public blob(memptr value)
       {
          // the value may be a proxy; make sure to unwrap and work with the real value
          if (BaseDataType.isProxy(value))
          {
             assign(value.val());
          }
          else
          {
             assign(value.asByteArray());
          }
       }

       /**
        * Constructor which initializes an instance from a raw object.
        *
        * @param   value
        *          Raw object from which data is copied.
        */
       public blob(raw value)
       {
          // the value may be a proxy; make sure to unwrap and work with the real value
          if (BaseDataType.isProxy(value))
          {
             assign(value.val());
          }
          else
          {
             assign(value.asByteArray());
          }
       }

       /**
        * Constructor which initializes an instance from a POLY value.
        *
        * @param   value
        *          POLY value from which data is copied.
        */
       public blob(BaseDataType value)
       {
          if (value.val() instanceof BinaryData)
          {
             assign(((BinaryData) value.val()).asByteArray());
          }
          else
          {
             UnimplementedFeature.missing("blob(POLY) constructor is not implemented!");
          }
       }

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

       /**
        * Read data from the SQL BLOB used to initialize this object.
        *
        * @throws  SQLException
        *          if there is an error reading data.
        */
       public void readData()
       throws SQLException
       {
          if (this.value != null)
          {
             // already initialized
             return;
          }

          if (sqlBlob == null)
          {
             setUnknown();

             return;
          }

          int len = (int) sqlBlob.length();
          if (len > 0)
          {
             assign(sqlBlob.getBytes(1, len));
          }

          sqlBlob.free();
          sqlBlob = null;
       }

       /**
        * Indicate whether this object manages character data (as opposed to binary data).
        *
        * @return  {@code false}.
        */
       public boolean isCharacterData()
       {
          return false;
       }

       /**
        * Sets the length of the buffer/array to a given length.  Depending on the sub-class, this
        * may result in the allocation of memory, or the extending/truncating of the backing array.
        * It is up to the sub-class to determine if an already allocated buffer/array will be
        * resized.
        * <p>
        * This implementation is a no-op.
        *
        * @param   len
        *          New length of the buffer/array.
        *
        * @return  The current instance is returned.
        */
       @Override
       public BinaryData setLength(long len)
       {
          return this;
       }

       /**
        * 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</code> is also different from being uninitialized.
        *
        * @return    <code>true</code> if this instance is uninitialized.
        */
       @Override
       public boolean isUninitialized()
       {
          return value == null;
       }

       /**
        * @see com.goldencode.p2j.util.BaseDataType#duplicate()
        *
        * This class is not undoable, so return a reference to this object.
        *
        * @return  This object.
        */
       @Override
       public blob duplicate()
       {
          return this;
       }

       /**
        * Creates a new instance of the same type that represents the {@code unknown value}.
        *
        * @return  An instance that represents the {@code unknown value}.
        */
       @Override
       public BaseDataType instantiateUnknown()
       {
          blob blb = new blob();
          blb.setUnknown();

          return blb;
       }

       /**
        * 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 blob();
       }

       /**
        * The function {@code STRING(blob-value, format)} always generates error 11382 in OE.
        *
        * @param   fmt
        *          The format string to use.
        *
        * @return  The formatted string.
        */
       @Override
       public String toString(String fmt)
       {
          ErrorManager.recordOrThrowError(11382);
          // INPUT/OUTPUT operations are not allowed with RAW, ROWID, MEMPTR, BLOB, CLOB or LONGCHAR type variables. (11382)

          return "";
       }

       /**
        * Creates a string representation of the instance data in a form that is compatible with the
        * {@code MESSAGE} language statement. Regardless the instance represents the {@code unknown} value or not,
        * a "?" character value will always be returned.
        *
        * @return  The 'message' formatted string.
        */
       @Override
       public String toStringMessage()
       {
          return "?"; // in 4GL STRING(tt1.blobField) will return the "?" value!

          // NOTE: the following code is only used for FWD debugging
          // if (isUnknown())
          // {
          //    return null;
          // }
          //
          // int len = value.length;
          // StringBuilder buf = new StringBuilder("[");
          // for (int i = 0; i < len; i++)
          // {
          //    if (i > 0)
          //    {
          //       buf.append(',');
          //    }
          //    buf.append(Integer.toString(value[i], 16));
          // }
          // buf.append(']');
          // return buf.toString();
       }

       /**
        * 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.
        */
       @Override
       public String toStringExport()
       {
          // TODO: implement
          return null;
       }

       /**
        * Return all or some of the contents of the current buffer as an array. This will be a copy
        * of the data if the given parameters indicate a subset of the full data; otherwise, it will
        * be the original, backing buffer.
        *
        * @param   pos
        *          Starting offset position.
        * @param   len
        *          Length of range to return.
        *
        * @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.
        */
       @Override
       public byte[] asByteArray(long pos, long len)
       {
          if (isUnknown())
          {
             return null;
          }

          if (pos == 0 && len == value.length)
          {
             return value;
          }

          int offset = (int) Math.max(0, pos);
          int length = (int) Math.min(len, value.length);

          byte[] copy = new byte[length];
          System.arraycopy(value, offset, copy, 0, length);

          return copy;
       }

       /**
        * 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.
        */
       @Override
       public void writeExternal(ObjectOutput out)
       throws IOException
       {
          super.writeExternal(out);
          writeByteArray(out, value);
          writeString(out, fieldName);
       }

       /**
        * 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.
        */
       @Override
       public void readExternal(ObjectInput in)
       throws IOException,
              ClassNotFoundException
       {
          super.readExternal(in);
          value = readByteArray(in);
          fieldName = readString(in);
       }

       /**
        * Release any resources associated with the instance.
        */
       @Override
       protected void deallocate()
       {
          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</code> if the length must be limited.
        */
       @Override
       protected boolean isLengthLimited()
       {
          return true;
       }

       /**
        * Determines the maximum length of the array of binary data stored in an
        * instance of this class.
        *
        * @return   The length limit for binary data or 0 if this class has no limit.
        */
       @Override
       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</code> bytes.
        *
        * @return   <code>true</code> if the length must automatically extended.
        */
       @Override
       protected boolean isAutoExtend()
       {
          return false;
       }

       /**
        * 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>
        * This implementation is a no-op.
        *
        * @param   len
        *          The output array length.
        */
       @Override
       protected void extendBytes(long len)
       {
       }

       /**
        * Replace the entire contents of the current buffer with the given array.
        *
        * @param   value
        *          The new contents.
        */
       @Override
       protected void replaceContents(byte[] value)
       {
          this.value = value;
          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</code>, then <code>null</code> will be returned.
        */
       @Override
       protected byte[] asByteArray()
       {
          if (isUnknown())
          {
             return null;
          }

          if (value == null)
          {
             return new byte[0];
          }

          return value;
       }

       /**
        * The actual size of the buffer (in bytes).
        *
        * @return  The length or 0 if uninitialized.
        */
       @Override
       protected long internalLength()
       {
          return isUnknown() ? 0 : value.length;
       }

       /**
        * The length of the buffer as reported to P4GL application.
        *
        * @return  The length or 0 if uninitialized.
        */
       @Override
       protected long visibleLength()
       {
          return internalLength();
       }

       /**
        * Returns the next index position into the array which is the <code>null</code> byte,
        * starting at a given index position.
        * <p>
        * This implementation is a no-op.
        *
        * @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</code> byte or the length
        *          of the array if no <code>null</code> byte exists. This implementation always
        *          returns 0.
        */
       @Override
       protected long findNextNull(long pos)
       {
          return 0;
       }

       /**
        * 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.
        */
       @Override
       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.
        */
       @Override
       protected void writeByte(byte val, long pos)
       {
          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</code> to force the returned data to be ordered by the endian-ness of the
        *           instance.
        *
        * @return   The byte range at that location.
        */
       @Override
       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(this.value, idx, data, 0, end - idx);

          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!
        * <p>
        * This implementation is a no-op.
        *
        * @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</code> to honor the endian-ness of the instance. The <code>data</code>
        *           array MUST be ordered in little-endian order if the endian flag is on.
        */
       @Override
       protected void writeByteRange(byte[] data, long pos, long len, boolean endian)
       {
       }

       /**
        * Write the given byte data into the data managed by this object, overwriting the data
        * that are at the given location, if any.
        * <p>
        * This implementation:
        * <ul>
        * <li>honors the {@code trim} option (i.e., any existing bytes beyond the end of the
        *     write are left in place if {@code trim} is {@code false} or truncated if {@code trim}
        *     is {@code true});</li>
        * <li>extends the target size if the write goes past the end of the existing value.</li>
        * </ul>
        *
        * @param   overlay
        *          Flag indicating if the OVERLAY option is used.
        * @param   data
        *          Data to be assigned to this object.
        * @param   offset
        *          Offset position (in characters) in the target large object.
        * @param   trim
        *          {@code true} to truncate any remaining data in the target large object; {@code
        *          false} to leave remaining data alone.
        */
       @Override
       public void write(boolean overlay, byte[] data, int offset, boolean trim)
       {
          if (overlay)
          {
             /*
             TODO: this error is not thrown if the source is longchar, only if is clob
             if (!isUnknown() && data != null && data.length > 0 && lengthOf() == 0)
             {
                ErrorManager.recordOrThrowError(11298, "COPY-LOB could not copy the source large object to the target", false);
                return;
             }
             */

             if (!isUnknown() && data != null && lengthOf() <= offset + data.length)
             {
                // TODO: error
             }
          }

          int dataLen;

          if (data == null || (dataLen = data.length) == 0 && !trim || offset == 0 && value == data)
          {
             // nothing to write or data already is stored in this object (we don't do a deep equals
             // check because it could be expensive and the common case is likely a full assignment
             // anyway, so don't take the hit here, just quickly check for object reference equality)

             if (!overlay)
             {
                if (data != null)
                {
                   assign(data);
                }
                else
                {
                   setUnknown();
                }
             }

             return;
          }

          int currentLen = value != null ? value.length : 0;

          // simple case: just replace existing value with data
          if (!overlay || isUnknown() || (offset == 0 && (currentLen <= dataLen || trim)))
          {
             assign(data);

             return;
          }

          // complex case: overlay data over existing data, possibly extending the length; truncate the array at
          //       the end of newly copied data if [trim] is true

          int endData = offset + dataLen;
          int newLen = trim ? endData : Math.max(currentLen, endData);
          byte[] buf = new byte[newLen];

          if (offset > 0)
          {
             // copy first portion from existing value into new buffer
             System.arraycopy(value, 0, buf, 0, offset);
          }

          // copy new data to its target offset in new buffer
          System.arraycopy(data, 0, buf, offset, dataLen);

          if (currentLen > endData)
          {
             // copy last portion from existing value into new buffer
             int remLen = newLen - endData;
             System.arraycopy(value, endData, buf, endData, remLen);
          }

          assign(buf);
       }

       /**
        * Assigns a blob value.
        *
        * @param value
        *        Value to assign.
        */
       public void assign(blob value)
       {
          assign((BaseDataType) value);
       }

       /**
        * Sets the {@code FieldReference} reference. This is only used when the object is obtained by a
        * dereferenciation ({@code ::}) operation.
        *
        * @param   fr
        *          The source of this LOB.
        */
       @Override
       public void setFieldReference(FieldReference fr)
       {
          this.fRef = fr;

          // save current field name
          if (fr != null && (fieldName == null || fieldName.isEmpty()))
          {
             fieldName = fr.getLegacyFieldName();
          }
       }

       /**
        * Obtain the {@code FieldReference} reference, if any.
        *
        * @return  The parent {@code FieldReference} from which this LOB was obtained by dereferenciation.
        */
       @Override
       public FieldReference getFieldReference()
       {
          return fRef;
       }

       /**
        * Gets the filed name associated with current {@code FieldReference} reference, if any.
        *
        * @return  The field name to be used in LOB file name creation.
        */
       public String getFieldName()
       {
          return fieldName;
       }
    }