PropertyMapper.java

/*
** Module   : PropertyMapper.java
** Abstract : Classes which map bean properties to configuration info needed for data import
**
** Copyright (c) 2005-2024, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
** 001 ECF 20050819   @22203 Moved initial version from ImportWorker.
**                           These classes were previously inner classes
**                           of that top level class, but were extracted
**                           to be shared between database and directory
**                           import tasks.
** 002 ECF 20051005   @22984 Added first class support for raw data type.
**                           Replace byte array processing from RawMapper
**                           and replaced with raw data type.
** 003 ECF 20060326   @25258 Modified to use new setter signatures for
**                           integer and decimal property mappers. These
**                           now accept NumberType instead of the more
**                           specific types.
** 004 ECF 20070905   @35339 Fixed type assertion. Added unknown value as
**                           a permissible type for all property mappers.
** 005 ECF 20080107   @36751 Added RowIDMapper inner class. Handles RECID
**                           and ROWID data (or the lack thereof). Added
**                           isExported() method to base class and added
**                           override in RowIDMapper.
** 006 GES 20080418   @38173 Moved from a lexer approach to using the
**                           equivalent runtime support for the Progress
**                           IMPORT language stmt.
** 007 ECF 20080625   @38930 Fixed RowIDMapper.createPlaceholder(). This
**                           method must return a data wrapper instance
**                           which represents unknown value, rather than
**                           throwing UnsupportedOperationException.
** 008 CA  20101125          Added getter support.
** 009 SVL 20120331          Use Text as the type which is expected by the
**                           setter method associated with the mapped
**                           character property.
** 010 OM  20130419          Added support for int64, longchar, datetime and datetime-tz.
**                           Introduced generics where needed.
** 011 OM  20130618          Use default constructors for unknown values of date_ values.
** 012 EVL 20160224          Javadoc fixes to make compatible with Oracle Java 8 for Solaris 10.
** 013 ECF 20160229          Fixed generic type in constructors. These were not picked up earlier
**                           because they only are called from TRPL.
** 014 ECF 20160311          Fixed regression introduced with #013.
** 015 ECF 20171016          Added subclasses.
** 016 ECF 20171101          Fixed regression in RecIDMapper.
** 017 ECF 20181215          Added LOB support.
** 018 OM  20200906          New ORM implementation.
** 019 CA  20200927          Use IdentityHashMap instead of plain map when the key is a Class.
**     OM  20210602          Public field member name change.
**     OM  20210619          Removed references to ImportWorker static field.
**     CA  20220520          Fixed dataPath when is not ending with file separator.
**     EVL 20221222          Changed methods type from private to public for preprocess(Blob|Clob).
**     IAS 20221125          Make location of LOB files configurable for import/
** 020 TJD 20240123          Java 17 compatibility updates
*/

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

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

import com.goldencode.p2j.persist.Record;
import com.goldencode.p2j.persist.orm.*;
import com.goldencode.p2j.util.*;

/**
 * Maps a DMO class property to a data value read at a specific position
 * in a data export file. Also manages a list of foreign class names
 * with which the current table/class is associated by a foreign key
 * association. This list will be empty if no foreign associations exist.
 * <p>
 * One instance of this class is created for each Progress field (or
 * subscripted data value within an "extent" field) represented in the
 * export data file being imported.
 * <p>
 * This class is the abstract base class for data type specific, concrete
 * implementations. Each of these is a public inner class of this class.
 * Each is static, so it can be instantiated externally, directly, using the
 * semantic:
 * <pre>
 *    PropertyMapper mapper = new PropertyMapper$&lt;data_type&gt;Mapper(...);
 * </pre>
 */
public final class PropertyMapper
implements SchemaParserTokenTypes
{
   /** The metadata for this property. */
   private final PropertyMeta propertyMeta;
   
   /** The 0-base offset of the property in the {@code Record}'s {@code data} array. */
   private final int propIndex;
   
   /**
    * The generator that creates a placeholder for reading an object of the type this property is
    * mapping. 
    */
   private final PlaceholderGenerator generator;
   
   /** Flags the exported properties. */
   private final boolean exported;
   
   /** The preprocessor in case of blob or clop properties. */
   private final Function<BaseDataType, BaseDataType> preprocessor;
   
   private static final Map<Class<? extends BaseDataType>, PlaceholderGenerator> generators = 
         new IdentityHashMap<>();
   
   static
   {
      generators.put(integer.class,    integer::new);
      generators.put(int64.class,      int64::new);
      generators.put(decimal.class,    decimal::new);
      generators.put(character.class,  character::new);
      generators.put(logical.class,    logical::new);
      generators.put(date.class,       date::new);       // unknown date
      generators.put(datetime.class,   datetime::new);   // unknown datetime
      generators.put(datetimetz.class, datetimetz::new); // unknown datetime-tz
      generators.put(raw.class,        raw::instantiateUnknownRaw);
      generators.put(handle.class,     handle::new);     // TODO: set [exported] to [false]?
      generators.put(recid.class,      recid::new);
      generators.put(rowid.class,      rowid::new);
      generators.put(blob.class,       character::new);
      generators.put(clob.class,       character::new);
   }
   
   /**
    * Constructor.
    *
    * @param   pm
    *          The {@code PropertyMeta} which gives information about this property.
    * @param   offset
    *          The 0-base offset of the property in the {@code Record}'s {@code data} array.
    */
   PropertyMapper(PropertyMeta pm, int offset)
   {
      Class<? extends BaseDataType> type = pm.getType();
      this.generator = generators.get(type);
      this.exported = (type != recid.class && type != rowid.class); // not exported in dump files
      this.propIndex = offset;
      this.propertyMeta = pm;
      
      if (type == blob.class)
      {
         preprocessor = PropertyMapper::preprocessBlob;
      }
      else if (type == clob.class)
      {
         preprocessor = PropertyMapper::preprocessClob;
      }
      else
      {
         preprocessor = null;
      }
   }
   
   /**
    * Perform any preprocessing of the data value about to be imported. This method is invoked
    * after the datum is read from the export file and before the DMO setter is invoked.
    * 
    * @param   datum
    *          A data value read from the export file.
    * 
    * @return  This implementation simply returns the datum unmodified. Subclasses which
    *          require different behavior must override this method. The returned value must be
    *          of a type that is expected by the DMO's setter method.
    */
   protected BaseDataType preprocessDatum(BaseDataType datum)
   {
      return preprocessor == null ? datum : preprocessor.apply(datum);
   }
   
   /**
    * Obtain the {@code PropertyMeta} for this property.
    *
    * @return  the {@code PropertyMeta} for this property.
    */
   public PropertyMeta getPropertyMeta()
   {
      return propertyMeta;
   }
   
   /**
    * Set a value into the mapped property of the given DMO instance. The data is directly set
    * into the {@code dmo}'s inner field, avoiding the standard {@code Record} state management.
    *
    * @param   dmo
    *          Data model object in which to set the value.
    * @param   value
    *          Data value to be stored in the mapped property.
    */
   final void setValue(Record dmo, BaseDataType value)
   {
      getPropertyMeta().getDataHandler().setField(
            dmo.getData(this), value, propIndex, propertyMeta);
   }
   
   /**
    * Checks whether the property mapped by this object is null for the specified {@code Record}.
    *
    * @param   dmo
    *          The record object in which to get the value.
    */
   final boolean isNull(Record dmo)
   {
      return dmo.getData(this)[propIndex] == null;
   }
   
   /**
    * Obtain the index of this property. This number also represents the index in the {@code data}
    * array of a record this object maps.
    * 
    * @return  the index of this property.
    */
   public int getIndex()
   {
      return propIndex;
   }
   
   /**
    * Report whether the property is expected to be found in exported data. Certain property types
    * are skipped when Progress exports data, and as such, these will not appear in an export
    * file. This method tells the record loader that the lexer should not expect to find any datum
    * for this property in the current row being imported.
    *
    * @return  {@code true} id the property is exported in dump files.
    */
   boolean isExported()
   {
      return exported;
   }
   
   /**
    * Instantiate an object which is the proper type to contain a value for
    * the mapped property, for a particular imported record. This storage
    * will be passed to the import processing and will be assigned the
    * value as read from the export file.
    *
    * @return  Placeholder storage of the correct type.
    */
   public BaseDataType createPlaceholder()
   {
      return generator.createPlaceholder();
   }

   /**
    * Perform any preprocessing of the data value about to be imported. This method is invoked
    * after the datum is read from the export file and before the DMO setter is invoked.
    * 
    * @param   datum
    *          A data value read from the export file.
    * 
    * @return  This implementation simply returns the datum unmodified. Subclasses which
    *          require different behavior must override this method. The returned value must be
    *          of a type that is expected by the DMO's setter method.
    */
   public static BaseDataType preprocessBlob(BaseDataType datum)
   {
      character locator = (character) datum;
      
      if (locator.isUnknown())
      {
         blob b = new blob();
         b.setUnknown();
         
         return b;
      }
      
      String name = locator.toStringMessage();
      
      byte[] data = readLobData(name);
      
      if (data == null)
      {
         blob b = new blob();
         b.setUnknown();
         
         return b;
      }
      
      return new blob(data);
   }
   
   /**
    * Perform any preprocessing of the data value about to be imported. This method is invoked
    * after the datum is read from the export file and before the DMO setter is invoked.
    * 
    * @param   datum
    *          A data value read from the export file.
    * 
    * @return  This implementation simply returns the datum unmodified. Subclasses which
    *          require different behavior must override this method. The returned value must be
    *          of a type that is expected by the DMO's setter method.
    */
   public static BaseDataType preprocessClob(BaseDataType datum)
   {
      character locator = (character) datum;
      
      if (locator.isUnknown())
      {
         return new clob();
      }
      
      String name = locator.toStringMessage();
      
      byte[] data = readLobData(name);
      
      if (data == null)
      {
         return new clob();
      }
      
      String[] parts = name.split("!");
      String charset = (parts.length > 1) ? parts[1] : null;
      
      if (charset != null)
      {
         try
         {
            return new clob(new String(data, charset));
         }
         catch (UnsupportedEncodingException exc)
         {
            // log warning, then use default encoding below
            System.out.println(
               "Charset '" + charset + "' for LOB '" + name + "' not supported; using default Java charset.");
         }
      }
      else
      {
         System.out.println(
               "No charset specified by LOB locator '" + name + "'; using default Java charset.");
      }
      
      return new clob(new String(data));
   }

   /**
    * Read a byte array from the file identified by the given locator.
    * 
    * @param   locator
    *          Name of LOB data file, relative to the lobs export directory.
    * 
    * @return  Byte array of the data, or {@code null} if the data could not be read, or was not
    *          found at the given location.
    */
   private static byte[] readLobData(String locator)
   {
      String path = ImportWorker.lobPath;
      if (!path.endsWith(File.separator))
      {
         path = path + File.separator;
      }
      path = path + locator;
      File file = new File(path);
      if (!file.exists())
      {
         System.out.println("LOB with locator '" + path + "' not found!");
         
         return null;
      }
      
      int len = (int) file.length();
      byte[] data = new byte[len];
      
      if (len > 0)
      {
         try (InputStream is = new FileInputStream(file))
         {
            long readData = is.read(data);
            if (len != readData)
            {
               System.out.println("LOB at '" + path + "' not fully read (" + readData + "/" + len + ")!");
            }
         }
         catch (IOException exc)
         {
            System.out.println("Error reading LOB at locator '" + path + "'!");
            
            return null;
         }
      }
      
      return data;
   }
   
   /**
    * Get a short string representation of this object. Only used for debugging/tracing.
    *
    * @return  a string representation of this object.
    */
   @Override
   public String toString()
   {
      return "PropertyMapper{" + propertyMeta.getName() +
            ": index=" + propIndex +
            ", type=" + propertyMeta.getType().getSimpleName() +
            ", exported=" + exported +
            '}';
   }
   
   /**
    * A functional interface that generated a specified kind of {@code BaseDataType} that will be
    * used as a placeholder for reading a value form a dump file.
    */
   @FunctionalInterface
   public interface PlaceholderGenerator {
      /**
       * Instantiate an object of a specific {@code BaseDataType} type.
       *
       * @return  Placeholder storage of the correct type.
       */
      abstract BaseDataType createPlaceholder();
   }
}