clob.java

/*
** Module   : clob.java
** Abstract : Data wrapper for SQL CLOB data.
**
** Copyright (c) 2018-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
** 001 ECF 20181214 Created initial version.
** 002 ECF 20190215 Added binaryEquals.
** 003 CA  20190514 Added clob(character) ctor.
** 004 CA  20190530 Added clob(BDT) ctor.
** 005 ECF 20190628 Implemented COPY-LOB runtime.
** 006 CA  20191119 clob initial state is unknown.
** 007 CA  20191203 Fixed overlay and unknown COPY-LOB related bugs.
** 008 OM  20210225 Pushed up binaryEquals() to LargeObject.
**     OM  20210312 Added parent FieldReference accessors in case the LOB was obtained by dereferenciation
**                  of a buffer field.
**     OM  20210328 Improved COPY-LOB engine and validations.
**     OM  20210404 Reworked codepage management for CLOB fields. These fields should know their CP from the
**                  moment they are created (before first assign).
**     AL2 20220328 Added proxy checks for BDT.
**     EVL 20221222 Adding runtime support for LOB fields export.
**     CA  20220613 Do not process empty codepage.
**     CA  20230207 Field name can be null when transferred via OpenClient calls.
** 009 CA  20230215 'instantiateDefault' now is implemented by each sub-class.
**     CA  20230215 'duplicate()' method returns the real type instead of BDT.
** 010 AL2 20250530 Added getIndependentFromContext.
** 011 TG  20250519 Added clob.getCodePage(clob).
*/

/*
** 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.sql.*;
import com.goldencode.p2j.persist.*;
import com.goldencode.util.*;

/**
 * Data wrapper type for SQL CLOB data.
 */
public class clob
extends longchar
{
   /** SQL CLOB object */
   private Clob sqlClob = null;
   
   /** The parent buffer field where this object was obtained by dereferenciation, if this is the case. */
   private FieldReference fRef;
   
   /** Flags this field if it was constructed with a custom CODEPAGE specified in the field definition. */
   private boolean customCP = false;
   
   /** The field name used with current field reference. */
   private String fieldName = null;

   /**
    * Default constructor.
    */
   public clob()
   {
      setUnknown();
   }
   
   /**
    * Copy constructor.
    * 
    * @param   value
    *          Clob to be copied.
    */
   public clob(clob value)
   {
      // the value may be a proxy; make sure to unwrap and work with the real value
      if (BaseDataType.isProxy(value))
      {
         if (value.val() instanceof clob)
         {
            value = (clob) value.val();
         }
         else
         {
            assign(value.val());
            return;
         }
      }
      if (value.unknown)
      {
         this.unknown = true;
         return;
      }
      
      this.unknown = false;
      this.value = value.value; // codePageConvert(value.value, value._getCodePage(), _getCodePage());
      this.sqlClob = value.sqlClob;
      this.fRef = null; //value.fRef
      
      // copy customCP/CP ?
   }
   
   /**
    * Constructor which is initialized from a database record.
    * 
    * @param   sqlClob
    *          SQL CLOB from which data is read.
    *          
    * @throws  SQLException
    *          if there is an error reading data.
    */
   public clob(Clob sqlClob)
   throws SQLException
   {
      this.sqlClob = sqlClob;
      
      // 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 clob(String value)
   {
      assign(value);
   }
   
   /**
    * Constructor which initializes an instance from the given data.
    * 
    * @param   value
    *          Data.
    * @param   codepage
    *          The codepage.
    */
   public clob(String value, String codepage)
   {
      customCP = (codepage != null && !codepage.isEmpty());
      longchar.fixCodePage(this, customCP ? codepage : I18nOps._getCPInternal(), true);
      
      assign(value);
   }
   
   /**
    * Constructor which initializes an instance from the given data.
    * 
    * @param   value
    *          Data.
    */
   public clob(character value)
   {
      assign(value);
   }
   
   /**
    * Constructor which initializes an instance from the given data.
    * 
    * @param   value
    *          Data.
    */
   public clob(longchar value)
   {
      assign(value);
   }
   
   /**
    * Constructor which initializes an instance from the given data.
    * 
    * @param   value
    *          Data.
    */
   public clob(BaseDataType value)
   {
      assign(value);
   }
   
   /**
    * Get the type.
    * 
    * @return type.
    */
   public Type getType()
   {
      return Type.CLOB;
   }
   
   /**
    * Read data from the SQL CLOB 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;
      }
      
      String s = null;
      
      if (sqlClob != null)
      {
         int len = (int) sqlClob.length();
         if (len > 0)
         {
            s = sqlClob.getSubString(1, len);
         }
         else if (len == 0)
         {
            s = "";
         }
         
         sqlClob.free();
         sqlClob = null;
      }
      
      assign(s);
   }
   
   /**
    * Return the value codepage fixed status.
    *
    * @return  Always {@code true}. The codepage for {@code clob}-s cannot be changed.
    */
   public boolean isCodePageFixed()
   {
      return true;
   }
   
   /**
    * Implementation of {@code IS-COLUMN-CODEPAGE} ABL function.
    *
    * @param   value
    *          The data to be tested.
    *
    * @return  {@code YES} only when {@code value} is a {@code CLOB} field defined with
    *          {@code IS-COLUMN-CODEPAGE} attribute.
    */
   public static logical isColumnCodepage(BaseDataType value)
   {
      if (value == null)
      {
         return new logical(); // or err 5729 ?
      }
      
      if (value instanceof clob)
      {
         return new logical(((clob) value).customCP);
      }
      
      ErrorManager.recordOrThrowError(5729);
      // Incompatible datatypes found during runtime conversion. (5729)
      return new logical();
   }

   /**
    * Returns the code page of a the specified clob variable.
    *
    * @param   value
    *          The value for which the codepage will be retrieved.
    *
    * @return  See above
    */
   public static character getCodePage(clob value)
   {
      return value.getCodePage();
   }
   
   /**
    * @see com.goldencode.p2j.util.BaseDataType#duplicate()
    * 
    * This class is not undoable, so return a reference to this object.
    * 
    * @return  This object.
    */
   @Override
   public clob 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()
   {
      clob clb = new clob();
      clb.setUnknown();
      
      return clb;
   }
   
   /**
    * 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 instantiateUnknown();
   }
   
   /**
    * Write the given character 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 characters 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. Ignored.
    */
   @Override
   public void write(boolean overlay, String data, int offset, boolean trim)
   {
      // if we are in overlay mode, fill with 'offset - 1' spaces to the right
      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 (data != null && (value == null || offset >= value.length()))
         {
            int rightFill = offset - (value == null ? 0 : value.length());
            assign(StringHelper.rightAlignText("", rightFill));
         }
      }

      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)))
      {
         assign(data);
         
         return;
      }
      
      // complex case: overlay data over existing data, possibly extending the length
      
      int endData = offset + dataLen;
      int newLen = trim ? endData : Math.max(currentLen, endData);
      char[] buf = new char[newLen];
      
      if (offset > 0)
      {
         // copy first portion from existing value into new buffer
         value.getChars(0, offset, buf, 0);
      }
      
      // copy new data to its target offset in new buffer
      data.getChars(0, dataLen, buf, offset);
      
      if (newLen > endData)
      {
         // copy last portion from existing value into new buffer
         value.getChars(endData, currentLen, buf, endData);
      }
      
      assign(new String(buf));
   }
   
   /**
    * 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;
   }

   /**
    * 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);
      out.writeObject(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);
      fieldName = (String) in.readObject();
   }

   /**
    * Assigns the value represented by {@code value} to this object. If needed (the source {@code value} has
    * a different codepage), a codepage conversion is performed.
    *
    * @param   value
    *          The source {@code Text} to be assigned.
    */
   @Override
   public void assign(Text 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;
      }
      super.assign(TextOps.codePageConvert(value, _getCodePage()));
   }
   
   /**
    * Assigns the value represented by {@code value} to this object. Since all {@code clob} fields have a
    * fixed codepage, a codepage conversion will occur.
    *
    * @param   value
    *          The source {@code longchar} / {@code clob} to be assigned.
    */
   @Override
   public void assign(longchar 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;
      }
      if (value.isUnknown())
      {
         setUnknown();
      }
      else
      {
         super.assign(TextOps.codePageConvert(value, _getCodePage()));
      }
   }

   /**
    * Creates a string representation of the instance data using the 'export'
    * format.  For the character data type the entire string is returned
    * enclosed in a pair of double quotes.  If the instance represents the
    * <code>unknown value</code>, a '?' will be returned.  Any instances of
    * double quotes inside the string will be doubled up on output.
    *
    * @return   The 'export' formatted string.
    */
   @Override
   public String toStringExport()
   {
      return isUnknown() ? "?" : value == null ? "" : value;
   }
   
   /**
    * Identify if this data is dependent upon the context local. This identifies
    * if the data is safe to be cached in cross-session places or the data should
    * not leak from the context. If is is not safe to be shared cross-session, then
    * this should return {@code null}. If other representation is suitable for caching,
    * then this can return another object that wraps internal values that are not
    * dependent upon the context (e.g. date, date-time, date-time-tz).
    * 
    * @return   Always. {@code null}.
    */
   @Override
   public Object getIndependentFromContext()
   {
      return null;
   }
}