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