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