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