# LobCopy.java

    /*
    ** Module   : LobCopy.java
    ** Abstract : A configurable LOB copy operation.
    **
    ** Copyright (c) 2019-2023, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
    ** 001 ECF 20190508 Created initial version to simplify API for COPY-LOB operations.
    ** 002 ECF 20190628 Added runtime implementation.
    ** 003 MED 20200331 Set codepage defaults based on 4GL defaults.
    **     CA  20200427 I18nOps.convmap2Java uses uppercase keys.
    ** 004 ME  20200528 Use getValue instead of toStringMessage to avoid unknown (?).
    **     OM  20210203 Added API to accept unknown parameter for fixCodePage() method.
    **     OM  20210312 Fixed error handling related to copy conversion errors. Rewrote copy operation.
    **     OM  20210328 Improved copy lob engine and validations.
    **     OM  20210404 Fine-tuned errors generated by edge-case in copy-lob statements.
    **     CA  20210609 Fixed a NPE when the target has no codepage set.
    **     ME  20210916 Fixed a NPE when destination codepage is null (not found).
    **     EVL 20220405 Target offset should be validated first for certain copy operations.
    ** 005 HC  20230118 Eliminated some of the uses of String.toUpperCase and/or String.toLowerCase
    **                  for performance.
    ** 006 EVL 20230317 Changed target longchar handling for 'overlay at' option.  The NULL byte in a source
    **                  data handling is different in this case when source is binary and target is longchar.
    **                  Fixed bug for memory allocation with recent changes.
    **     EVL 20230320 Optimized memory usage for character lob target with 'overlay at' option.  We can avoid
    **                  allocation of the another byte array.  Just use a part of old one to create target.
    **     EVL 20230320 Fixed some deviations found with recent testcases.  The source data array 0 length has
    **                  the same effect as if data array pointer is NULL.  Also the target longchar should emit
    **                  12012 error for any source data array length if data was not validated.
    */

    /*
    ** This program is free software: you can redistribute it and/or modify
    ** it under the terms of the GNU Affero General Public License as
    ** published by the Free Software Foundation, either version 3 of the
    ** License, or (at your option) any later version.
    **
    ** This program is distributed in the hope that it will be useful,
    ** but WITHOUT ANY WARRANTY; without even the implied warranty of
    ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    ** GNU Affero General Public License for more details.
    **
    ** You may find a copy of the GNU Affero GPL version 3 at the following
    ** location: https://www.gnu.org/licenses/agpl-3.0.en.html
    **
    ** Additional terms under GNU Affero GPL version 3 section 7:
    **
    **   Under Section 7 of the GNU Affero GPL version 3, the following additional
    **   terms apply to the works covered under the License.  These additional terms
    **   are non-permissive additional terms allowed under Section 7 of the GNU
    **   Affero GPL version 3 and may not be removed by you.
    **
    **   0. Attribution Requirement.
    **
    **     You must preserve all legal notices or author attributions in the covered
    **     work or Appropriate Legal Notices displayed by works containing the covered
    **     work.  You may not remove from the covered work any author or developer
    **     credit already included within the covered work.
    **
    **   1. No License To Use Trademarks.
    **
    **     This license does not grant any license or rights to use the trademarks
    **     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
    **     of Golden Code Development Corporation. You are not authorized to use the
    **     name Golden Code, FWD, or the names of any author or contributor, for
    **     publicity purposes without written authorization.
    **
    **   2. No Misrepresentation of Affiliation.
    **
    **     You may not represent yourself as Golden Code Development Corporation or FWD.
    **
    **     You may not represent yourself for publicity purposes as associated with
    **     Golden Code Development Corporation, FWD, or any author or contributor to
    **     the covered work, without written authorization.
    **
    **   3. No Misrepresentation of Source or Origin.
    **
    **     You may not represent the covered work as solely your work.  All modified
    **     versions of the covered work must be marked in a reasonable way to make it
    **     clear that the modified work is not originating from Golden Code Development
    **     Corporation or FWD.  All modified versions must contain the notices of
    **     attribution required in this license.
    */

    package com.goldencode.p2j.util;

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

    import static com.goldencode.p2j.util.LobCopyParameter.*;

    /**
     * Object which performs a large object (LOB) copy operation. All configuration methods return
     * the current object instance, to allow method chaining.
     */
    public class LobCopy
    {
       /** Source DMO to be copied */
       private final LobCopyInput source;

       /** Target DMO into which to copy */
       private final LobCopyOutput target;

       /** Source content's code page; {@code null} for binary data */
       private String sourceCodePage = null;

       /** Test whether the CONVERT SOURCE CODEPAGE option was used, even if with unknown value. */
       private boolean sourceCodePageSet = false;

       /** Target content's code page; {@code null} for binary data */
       private String targetCodePage = null;

       /**
        * Constructor which accepts a source and target large object on which to perform the copy.
        *
        * @param   source
        *          Source object to be copied.
        * @param   target
        *          Target object into which to copy.
        */
       public LobCopy(LobCopyInput source, LobCopyOutput target)
       {
          this.source = source;
          this.target = target;
       }

       /**
        * Assert that the given parameter is an instance of {@link LargeObject}.
        *
        * @param   obj
        *          An object expected to be of type {@code LargeObject}.
        *
        * @return  {@code obj} cast to {@code LargeObject}.
        *
        * @throws  ConditionException
        *          if {@code lob} is not an instance of {@code LargeObject} and we are not in silent
        *          error mode.
        */
       static LargeObject assertParameterType(Object obj)
       {
          if (!(obj instanceof LargeObject))
          {
             ErrorManager.recordOrThrowError(
                   11303,
                   "COPY-LOB object must be a large object (BLOB, MEMPTR, etc.)");

             // Note: we will get here if we are in silent error mode. We have to return quietly here; otherwise,
             //       we will fall through and raise ClassCastException. We abort in the run method if a parameter
             //       is not correct.
             return null;
          }

          return (LargeObject) obj;
       }

       /**
        * Get the default code page of the given LOB. If the LOB does not itself maintain an explicit
        * code page, the equivalent of {@code -cpinternal} is returned.
        *
        * @param   lob
        *          Large object.
        *
        * @return  Default 4GL code page name.
        */
       static String getLobDefaultCodePage(LargeObject lob)
       {
          String cp = null;

          // BLOB has no defined encoding
          if (lob != null && !(lob instanceof blob))
          {
             // MEMPTR uses internal codepage
             cp = lob.isCharacterData() ? lob._getCodePage() : I18nOps._getCPInternal();
          }

          return cp;
       }

       /**
        * Set the source's code page.
        *
        * @param   sourceCodePage
        *          Source code page.
        *
        * @return  This object instance.
        */
       public LobCopy sourceCodePage(Text sourceCodePage)
       {
          if (sourceCodePage.isUnknown())
          {
             return this;
          }

          return sourceCodePage(sourceCodePage.getValue());
       }

       /**
        * Set the source's code page.
        *
        * @param   sourceCodePage
        *          Source code page.
        *
        * @return  This object instance.
        */
       public LobCopy sourceCodePage(String sourceCodePage)
       {
          this.sourceCodePage = sourceCodePage;
          this.sourceCodePageSet = true;

          return this;
       }

       /**
        * Set the target's code page.
        *
        * @param   unk
        *          Unknown target code page.
        *
        * @return  This object instance.
        */
       public LobCopy targetCodePage(unknown unk)
       {
          return targetCodePage(new character());
       }

       /**
        * Set the target's code page.
        *
        * @param   targetCodePage
        *          Target code page.
        *
        * @return  This object instance.
        */
       public LobCopy targetCodePage(Text targetCodePage)
       {
          if (targetCodePage.isUnknown())
          {
             return this;
          }

          return targetCodePage(targetCodePage.getValue());
       }

       /**
        * Set the target's code page.
        *
        * @param   targetCodePage
        *          Target code page.
        *
        * @return  This object instance.
        */
       public LobCopy targetCodePage(String targetCodePage)
       {
          this.targetCodePage = targetCodePage;

          return this;
       }

       /**
        * Perform the copy. This method should only be invoked once all configuration methods
        * have been invoked. In the typical use pattern, this method is called at the end of the
        * method chain which starts with {@code new LobCopy(source, target)}.
        */
       public void run()
       {
          if (!source.isLargeObject() || !target.isLargeObject())
          {
             // abort; ErrorManager will have recorded the error in assertParameterType
             return;
          }

          int sourceType = source.getObjectType();
          int targetType = target.getObjectType();

          if (sourceCodePageSet && sourceType == TYPE_LONGCHAR)
          {
             ErrorManager.recordOrThrowError(11339); // this is actually a compile time error
             // The source codepage can not be specified for a CLOB/LONGCHAR field
             return;
          }

          if (sourceType + targetType == TYPE_CLOB + TYPE_BLOB)
          {
             ErrorManager.recordOrThrowError(11851);
             // COPY-LOB operations between BLOB and CLOB fields are not supported. (11851)
             return;
          }

          // to be checked first
          if (!target.validateOffset(sourceType))
          {
             return;
          }
          else if (!source.validateOffset(targetType))
          {
             return;
          }
          else if (!target.validate(sourceType))
          {
             return;
          }
          else if (!source.validate(targetType))
          {
             return;
          }

          // we are working with character data; possibly need code page conversion
          if (source.isCharacterData())
          {
             String sourceText = source.readString(source.getCodePage());

             if (target.isCharacterData())
             {
                // case 1:     both [source] and [target] are character
                // solution:   copy text information from source to target. Each of them will keep its CP
                if (targetCodePage != null && !targetCodePage.equals(target.getCodePage()))
                {
                   ErrorManager.recordOrThrowError(11341);
                   // The target codepage conflicts with the longchar's fixed codepage. (11341)
                   return;
                }

                if (targetCodePage == null && sourceCodePage == null) // NO-CONVERT ?
                {
    //               if (!I18nOps.canConvert(source.getCodePage(), target.getCodePage()))
    //               {
    //                  ErrorManager.recordOrThrowError(11395);
    //                  // Large object assign or copy failed. (11395)
    //                  return;
    //               }
                }

                target.write(sourceText, targetCodePage);
             }
             else
             {
                // case 2:     [source] is character but [target] is binary
                // solution:   write into the [target] the text of [source] in desired CP
                String destCP = (targetCodePage != null) ? targetCodePage
                      : ((sourceCodePage != null) ? sourceCodePage : source.getCodePage());
                if (destCP == null)
                {
                   destCP = I18nOps._getCPInternal();
                }
                String destCharset = I18nOps.convmap2Java.get(destCP);
                try
                {
                   target.write(sourceText == null ? null : sourceText.getBytes(destCharset));
                }
                catch (Exception e)
                {
                   ErrorManager.recordOrThrowError(912, destCP, "convmap.p");
                   // Code page attribute table for <code-page> was not found in <filename>.
                }
             }
          }
          else
          {

             byte[] data;
             try
             {
                data = source.readBytes();
             }
             catch (Exception e)
             {
                if (targetType == TYPE_FILE)
                {
                   ErrorManager.recordOrThrowError(11350, ((TargetLobFile) target).getFilename(), "");
                   // Write to file '' failed during COPY-LOB (11350)
                }
                else
                {
                   ErrorManager.recordOrThrowError(11334);
                   // Offset supplied for source large object is greater than object size. (11334)
                }
                return;
             }

             if (target.isCharacterData())
             {
                // case 3:     [source] is binary but [target] is character
                // solution:   use the configured CP to decode the text in binary source and assign it to dest

                if (targetCodePage != null &&
                    target.getCodePage() != null &&
                    !targetCodePage.equals(target.getCodePage()))
                {
                   ErrorManager.recordOrThrowError(11341);
                   // The target codepage conflicts with the longchar's fixed codepage. (11341)
                   return;
                }

                String scp = sourceCodePage != null ? sourceCodePage : source.getCodePage();
                if (scp == null)
                {
                   scp = I18nOps._getCPInternal();
                }
                String sourceCharset = I18nOps.convmap2Java.get(scp);

                String text = null;
                if (data != null && data.length != 0)
                {
                   // the effective data length can be changed below
                   int dataLength = data.length;
                   // the target is valid TargetLob
                   if (target instanceof TargetLob)
                   {
                      TargetLob tlob = (TargetLob)target;
                      int newLobLength = -1;
                      // and uses 'overlay at' option
                      if (tlob.isOverlay())
                      {
                         // remember initial data length
                         int dataLengthOrig = data.length;
                         // preprocess source data to remove NULL bytes, the effective array length
                         // could be changed
                         dataLength = getDataLengthForOverlay(data);
                         if (dataLength == 0)
                         {
                            // if the new data is empty, need to trim target lob
                            tlob.trim();
                            // but the length of the target object should be overridden
                            newLobLength = dataLengthOrig;
                         }
                      }
                      // sets up new internal target lob length, -1 resets this internal value to use regular
                      // approach of character based object length calculation
                      tlob.setOverrideLength(newLobLength);
                   }

                   if (sourceCharset != null)
                   {
                      if (!I18nOps.prevalidateData(data, 0, dataLength, scp))
                      {
                         if (sourceType == TYPE_MEMPTR && targetType == TYPE_LONGCHAR)
                         {
                            ErrorManager.recordOrThrowError(12012, scp.toUpperCase(), "");
                            // Invalid character data found in MEMPTR for codepage  (12012)
                         }
                         else if (sourceType == TYPE_BLOB && targetType == TYPE_LONGCHAR && data.length <= 1)
                         {
                            // NOTE: sometimes error 11316 is thrown here:
                            //       I could not find any documentation on this error. :(
                            ErrorManager.recordOrThrowError(11316, scp.toUpperCase(), "");
                            // Could not obtain LONGCHAR. (11316)
                         }
                         else
                         {
                            ErrorManager.recordOrThrowError(12008, scp.toUpperCase(), "");
                            // Invalid character code found in data for codepage  (12008)
                         }

                         return;
                      }

                      try
                      {
                         text = new String(data, 0, dataLength, sourceCharset);
                      }
                      catch (UnsupportedEncodingException exc)
                      {
                         ErrorManager.recordOrThrowError(912, scp, "convmap.p");
                         // Code page attribute table for <code-page> was not found in <filename>.
                         return;
                      }
                   }
                   else
                   {
                      // failed to convert source CP to a charset. Try to autodetect it using BOM, if one exist
                      text = I18nOps.getStringWithBomCheck(data, 0, dataLength);
                   }
                }
                target.write(text, targetCodePage);
             }
             else
             {
                // case 4:     both [source] and [target] are binary
                // solution:   just copy plain binary, without conversion whatsoever
                target.write(data);
             }
          }
       }

       /**
        * Gets data length for active 'overlay at' option.  The NULL bytes are valid in this case and can be
        * used to make new data length for array content before first NULL byte.
        *
        * @param   data
        *          The byte array to process.
        *
        * @return  The effective data length.
        */
       private int getDataLengthForOverlay(byte[] data)
       {
          int lengthRes;

          if (data == null || data.length == 0)
          {
             return 0;
          }

          for (lengthRes = 0; lengthRes < data.length; lengthRes++)
          {
             if (data[lengthRes] == 0)
             {
                break;
             }
          }

          return lengthRes;
       }
    }