# 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();
       }
    }