DirectAccessHelper.java
/*
** Module : DirectAccessHelper.java
** Abstract : Middleware between FWD persistence layer and underlying database with direct access interface.
**
** Copyright (c) 2023-2024, Golden Code Development Corporation.
**
** -#- -I- --Date-- -----------------------------------Description-----------------------------------
** 001 AL2 20230210 Created initial version.
** 002 AL2 20230320 Avoid hydration if record is already found in the session cache by recid.
** 003 AL2 20230517 Adapt to new row structure constructor signature.
** 004 AL2 20231013 Added nextPrimaryKey.
** 005 AL2 20230926 Added hasRecords.
** 006 AL2 20231116 Added support for clearing multiplex in FWD-H2.
** 007 DDF 20231116 Normalize rowid and longchar arguments.
** DDF 20231117 Normalize date, datetime and unknown value arguments.
** 008 AL2 20231211 Use FullRowStructure to signal the fact that the full record was extracted.
** 009 RAA 20230803 Added support for notifying H2 to handle unique index validation.
** RAA 20231127 Made startValidate return DirectAccessDriver.
** 010 CA 20240422 For a NEW temp record which was not flushed and is being deleted or rolled back in the
** transaction, the allocated primary key row (which is an empty template) to reserve this
** primary key, needs to be explicitly removed.
** 011 AL2 20240512 Remove bulk option from nextPrimaryKey.
** 012 ES 20240626 Refactor if conditionals in normalizeValue method.
** 013 CA 20240627 'clearPrimaryKey' will return false if the PK is not found, instead of throwing NPE.
** 014 AS 20240808 Throw DirectAccessException in findByUniqueIndex if more than one record can be accessed.
** 015 AS 20241121 Added normalization of FieldReference objects in normalizeValue.
*/
/*
** 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.persist.orm;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.h2.embedded.DirectAccessDriver;
import org.h2.embedded.DirectAccessResponse;
import org.h2.jdbc.JdbcConnection;
import com.goldencode.p2j.oo.lang._BaseObject_;
import com.goldencode.p2j.persist.PersistenceException;
import com.goldencode.p2j.persist.Record;
import com.goldencode.p2j.persist.TempRecord;
import com.goldencode.p2j.persist.UniqueIndexLookup;
import com.goldencode.p2j.persist.FieldReference;
import com.goldencode.p2j.util.*;
/**
* Common point where all the direct access interface is used inside FWD. Any attempt
* to use a database driver internal structures should be done through this gateway.
*/
public class DirectAccessHelper
{
/**
* Direct access attempt to retrieve the next primary key available for the mentioned
* table and multiplex. The target table should be defined with {@code MULTIPLEXED}
* to allow the generation of a proper index to be used by this routine.
*
* In essence, this retrieves the next primary key and reserves its spot. Unless a further
* insert is done with that key, the reserved slot will leak. Use this carefully.
*
* If you want to do a bulk insert, use the bulk option. This will no reserve any spot as
* the number of intended keys is unknown. If bulk is set, this will return the first key
* that is available and no higher keys are already reserved / used. Such key should be used
* immediately, otherwise the previous mentioned invariant may not hold (an insert happens in
* the mean-time).
*
* @param session
* The session with a database with direct access interface.
* @param tableName
* The name of the table from which the next key is needed.
* @param multiplex
* The multiplex for which the next key is needed. This is mandatory, as 4GL does
* the reclaiming of keys per-multiplex (e.g. one multiplex won't reclaim the keys
* from another multiplex, even if they are in the same physical table).
*
* @return The next key available according to the provided table and multiplex. This would be
* reclaimed or not / reserved or not depending on the {@code bulk} value.
*
* @throws DirectAccessException
* An exception is thrown if the table doesn't exist.
*
*/
public static long nextPrimaryKey(Session session, String tableName, int multiplex)
throws DirectAccessException
{
try
{
JdbcConnection conn = getH2Connection(session);
DirectAccessDriver driver = conn.getDirectAccessDriver();
return driver.nextPrimaryKey(tableName, multiplex);
}
catch (SQLException e)
{
throw new DirectAccessException("Error direct accessing for finding next primary key", e);
}
}
/**
* When a {@link DmoState#NEW} record is being deleted or undone in the same transaction, without having
* a chance to occupy the reserved slot (by primary key) via an explicit or implicit flush, that slot
* needs to be cleared.
*
* @param session
* The session with a database with direct access interface.
* @param dmo
* The temporary DMO instance in {@link DmoState#NEW} state.
*
* @return {@code true} if the primary key was found and removed from this multiplex.
*
* @throws DirectAccessException
* An exception is thrown if the table doesn't exist.
*/
public static boolean clearPrimaryKey(Session session, TempRecord dmo)
throws DirectAccessException
{
try
{
DmoMeta meta = DmoMetadataManager.getDmoInfo(dmo.getClass());
String tableName = meta.getSqlTableName();
int multiplex = dmo._multiplex();
long primaryKey = dmo.primaryKey();
JdbcConnection conn = getH2Connection(session);
DirectAccessDriver driver = conn.getDirectAccessDriver();
return driver.clearPrimaryKey(tableName, multiplex, primaryKey);
}
catch (SQLException e)
{
throw new DirectAccessException("Error direct accessing for clearing primary key for undone record", e);
}
}
/**
* Retrieve the number of rows that are currently in a specified table.
*
* @param session
* The session with a database with direct access interface.
* @param tableName
* The name of the table for which we want the number of rows.
* @param multiplex
* The multiplex for which the query is done.
*
* @return The number of rows currently in the specified table.
*/
public static boolean hasRecords(Session session, String tableName, int multiplex)
throws DirectAccessException
{
try
{
JdbcConnection conn = getH2Connection(session);
DirectAccessDriver driver = conn.getDirectAccessDriver();
return driver.hasRecords(tableName, multiplex);
}
catch (SQLException e)
{
throw new DirectAccessException("Error direct accessing for finding the row count", e);
}
}
/**
* Direct access attempt on database structures to retrieve a single record based on recid.
*
* @param session
* The session with a database with direct access interface.
* @param meta
* The meta of the table to be queried.
* @param recid
* The value of the recid for the record to be retrieved.
*
* @return A hydrated record directly from the underlying direct access interface.
*
* @throws DirectAccessException
* An exception is thrown if the table doesn't exist. An exception is also
* thrown if the prerequisites are not met (no recid primary key).
*
*/
public static Record findByRowid(Session session, DmoMeta meta, long recid)
throws DirectAccessException
{
try
{
JdbcConnection conn = getH2Connection(session);
DirectAccessDriver driver = conn.getDirectAccessDriver();
String tableName = meta.getSqlTableName();
DirectAccessResponse response = driver.getRow(tableName, recid);
return interpretResponse(session, meta, response);
}
catch (SQLException | PersistenceException e)
{
throw new DirectAccessException("Error direct accessing for finding record by rowid", e);
}
}
/**
* Direct access attempt on database structures to retrieve a single record based on a
* unique index.
*
* @param session
* The session with a database with direct access interface.
* @param meta
* The meta of the table to be queried.
* @param lookup
* Specific object which contains low-level information about the properties
* and values needed for unique index lookup.
*
* @return A hydrated record directly from the underlying direct access interface.
*
* @throws DirectAccessException
* An exception is thrown if the table doesn't exist. Also, if the properties don't
* match any unique index, then an exception is thrown.
*/
public static Record findByUniqueIndex(Session session, DmoMeta meta, UniqueIndexLookup lookup)
throws DirectAccessException
{
try
{
JdbcConnection conn = getH2Connection(session);
DirectAccessDriver driver = conn.getDirectAccessDriver();
String tableName = meta.getSqlTableName();
Object[] values = lookup.getValues();
for (int i = 0; i < values.length; i++)
{
values[i] = normalizeValue(values[i]);
}
DirectAccessResponse response = driver.getUniqueRow(tableName, lookup.getProperties(), values);
if (response.getAmbiguous())
{
throw new PersistenceException("Unique result constraint violation.");
}
return interpretResponse(session, meta, response);
}
catch (SQLException | PersistenceException e)
{
throw new DirectAccessException("Error direct accessing for finding record by unique index", e);
}
}
/**
* Forcefully clear the multiplex in the FWD-H2. This is required, as the reclaiming process stores
* that last batch active for each multiplex. This way, we properly reclaim the ids per-multiplex.
* To avoid leaking, we need to notify FWD-H2 that storing such information is no longer needed.
*
* @param session
* The session with a database with direct access interface.
* @param tableName
* The name of the table for which we wan't to kill the multiplex
* @param multiplex
* The multiplex whose scope was closed.
*/
public static void killMultiplex(Session session, String tableName, int multiplex)
{
JdbcConnection conn = getH2Connection(session);
DirectAccessDriver driver = conn.getDirectAccessDriver();
try
{
driver.killMultiplex(tableName, multiplex);
}
catch (SQLException e)
{
throw new IllegalStateException("Error direct accessing for killing multiplex", e);
}
}
/**
* Check if the provided session allows a direct access interface.
*
* @param session
* The session with a database with direct access interface.
*
* @return {@code true} if this session allows direct access.
*/
public static boolean hasDirectAccess(Session session)
{
return getH2Connection(session) != null;
}
/**
* Mark that server-side unique index validation has stopped and notify H2 that it should handle
* the process.
*
* @param session
* The session with a database with direct access interface.
*
* @return DirectAccessDriver
* The H2 driver.
*/
public static DirectAccessDriver startValidate(Session session)
{
JdbcConnection conn = getH2Connection(session);
if (conn == null)
{
return null;
}
DirectAccessDriver driver = conn.getDirectAccessDriver();
driver.startValidate();
return driver;
}
/**
* Mark that H2 no longer needs to handle unique index validation, as it will be done server-side.
*
* @param session
* The session with a database with direct access interface.
*/
public static void stopValidate(Session session)
{
JdbcConnection conn = getH2Connection(session);
if (conn == null)
{
return;
}
conn.getDirectAccessDriver().stopValidate();
}
/**
* Normalize the FWD specific data types into Java native types. This is needed
* in order to interface with the underlying database through direct access.
*
* @param arg
* The object to be "normalized" (converted to Java native type)
*
* @return A Java native type that can be used by the direct access interface.
*
* @throws DirectAccessException
* An exception is thrown when a provided argument couldn't be converted to a
* Java representation which can be passed to the H2 engine.
*/
private static Object normalizeValue(Object arg)
throws DirectAccessException
{
boolean expected = false;
if (arg == null)
{
return null;
}
if (arg instanceof Number ||
arg instanceof Boolean ||
arg instanceof String)
{
// already normalized
return arg;
}
if (arg instanceof FieldReference)
{
arg = ((FieldReference) arg).get();
}
if (arg instanceof BaseDataType)
{
if (((BaseDataType) arg).isUnknown())
{
return null;
}
if (arg.getClass() == integer.class)
{
return ((integer) arg).toJavaIntegerType();
}
if (arg.getClass() == int64.class)
{
return ((int64) arg).toJavaLongType();
}
if (arg.getClass() == logical.class)
{
return ((logical) arg).toJavaType();
}
if (arg.getClass() == handle.class)
{
return ((handle) arg).getResourceId();
}
if (arg.getClass() == character.class)
{
return ((character) arg).toJavaType();
}
if (arg.getClass() == longchar.class)
{
return ((longchar) arg).toJavaType();
}
if (arg.getClass() == decimal.class)
{
return ((decimal) arg).toJavaType();
}
if (arg.getClass() == date.class)
{
return ((date) arg).dateValue();
}
if (arg.getClass() == datetime.class)
{
return ((datetime) arg).dateValue();
}
if (arg.getClass() == rowid.class)
{
return((rowid) arg).getValue();
}
if (arg.getClass() == recid.class)
{
return((recid) arg).getValue();
}
if (arg.getClass() == object.class)
{
return (object.resourceId((object<? extends _BaseObject_>) arg));
}
if (arg instanceof datetimetz)
{
expected = true;
}
}
throw new DirectAccessException("Can't normalize FWD value to use it with direct access.", expected);
}
/**
* Core method of hydrating a record from the retrieved result set. Usually, the result-set
* is a dummy container of row structures from the underlying database. Make sure these
* are converted to FWD structures by hydrating.
*
* @param session
* The session with a database with direct access interface.
* @param meta
* The meta of the table to be queried.
* @param response
* The response returned by the direct-access interface.
*
* @return A hydrated record directly from the underlying direct access interface.
*
* @throws SQLException
* An exception is thrown when anything goes wrong with the dummy JDBC API (result-set).
* @throws PersistenceException
* This method does a session cache look-up. If this goes wrong, an exception is thrown.
*
*/
private static Record interpretResponse(Session session, DmoMeta meta, DirectAccessResponse response)
throws PersistenceException,
SQLException
{
if (response.recid == null)
{
// not found
return null;
}
Record dmo = session.getCached(meta.getImplementationClass(), response.recid);
if (dmo != null && !dmo.checkState(DmoState.STALE) && !dmo.checkState(DmoState.INCOMPLETE))
{
return dmo;
}
ResultSet rs = response.getResultSet();
if (rs.next())
{
RowStructure rowStructure = new FullRowStructure(meta);
return (Record) SQLQuery.hydrateRecord(rs, rowStructure, 1, session);
}
else
{
return null;
}
}
/**
* Retrieve the H2 connection straight from the provided session. This returns {@code null}
* if the H2 connection doesn't allows direct access interface of if this is not a H2 connection.
* This should be refactored maybe to be more generic (not necessarily that close related to H2)
*
* @param session
* The session with a database with direct access interface.
*
* @return A H2 specific connection.
*/
private static JdbcConnection getH2Connection(Session session)
{
if (session == null)
{
return null;
}
Connection conn;
try
{
conn = session.getConnection();
}
catch (PersistenceException e)
{
return null;
}
if (TempTableDataSourceProvider.isProxy(conn))
{
conn = TempTableDataSourceProvider.unwrapProxy(conn);
}
if (conn != null &&
conn instanceof JdbcConnection &&
((JdbcConnection) conn).getDirectAccessDriver() != null)
{
return (JdbcConnection) conn;
}
return null;
}
}