TableResultSet.java
/*
** Module : TableResultSet.java
** Abstract : A result set used to serialize a table data and send it to a remote side.
**
** Copyright (c) 2013-2023, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------Description----------------------------------
** 001 CA 20130610 Created initial version.
** 002 CA 20130628 Added possibility to retrieve the data as a ResultSet instance.
** 003 OM 20130705 Fixed date/datetime results with time functions.
** 004 CA 20140110 Added access to the metadata received from the remote side.
** 005 ECF 20140623 Removed Hibernate dependency from read capabilities. This is a quick and dirty
** workaround which removes the ResultSetMetaData.getColumnType implementation.
** Further work is required to re-implement this feature without direct reliance
** on the P2J UserType subclasses.
** 006 ECF 20140624 Re-enabled ResultSetMetaData.getColumnType with new implementation.
** 007 ECF 20140626 Replaced GCD proxy with J2SE proxy in cases where we have no concrete parent.
** 008 CA 20140722 Improved the ResultSet proxy to return only Java types. Some other misc
** fixes/improvements: check if result set is closed before executing APIs,
** allow only P2J wrapper types to be resolved by the getters, better handling of
** null/unknown values.
** 009 CA 20140812 Added warnings to APIs which should not be used outside of P2J internal code.
** Fixed asResultSet problem which did not allow more than one walk over the
** results. Throw SQLException if the received index is not valid.
** 010 OM 20141216 Replaced the returned ColumnName as legacy name instead of internal "fieldN".
** 011 OM 20150316 Adjusted the BigDecimal scale to match the P4GL message precision in
** getObject() method.
** 012 EVL 20160223 Javadoc fixes to make compatible with Oracle Java 8 for Solaris 10.
** 013 IAS 20160331 Fixed setScale()
** 014 CA 20190628 Added legacy table name.
** 015 OM 20190708 Extracted common interface with DataSetContainer in MassiveContainer.
** 016 CA 20191119 Added support for before-table.
** 017 OM 20200906 New ORM implementation.
** 018 SVL 20210701 Added clearRows.
** 019 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
*/
/*
** 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;
import java.lang.reflect.*;
import java.math.*;
import java.sql.*;
import java.util.*;
import com.goldencode.p2j.persist.hql.*;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.logging.*;
/**
* Provides the main mechanism of passing table-like metadata and data which the remote side will
* interpret as a temporary table. The remote side will use the sent properties (and their other
* settings, as type and extent) to dynamically build the DMO interface, implementation class and
* .hbm.xml configuration file.
* <p>
* As transport, any instance of this class will be wrapped in a {@link TableWrapper} instance
* internally by P2J.
* <p>
* <strong>Note:</strong> In the process of building the internal data structures for dynamic
* temp-tables, internal generic indexed names are used for columns. To obtain the expected column
* name, the metadata will return the column's legacy name instead of internal database name.
*/
public abstract class TableResultSet
extends MassiveContainer
{
/** logger */
private static final CentralLogger LOG = CentralLogger.get(TableResultSet.class.getName());
/**
* The data sent by a remote side, after the remote appserver call, when this table is in
* OUTPUT or INPUT-OUTPUT mode.
*/
private List<Object[]> rows;
/** A set of meta information for each row, as given by {@link TempRecord#toArray()}. */
private List<Object[]> rowsMeta;
/**
* The metadata sent by a remote side, after the remote appserver call, when this table is in
* OUTPUT or INPUT-OUTPUT mode.
*/
private List<PropertyDefinition> properties;
/**
* Default c'tor, explicitly added to allow instances of this class to be created on
* deserialization. Not for public use.
*/
public TableResultSet()
{
}
/**
* Create a new instance of this result set.
*
* @param input
* Flag indicating this table is sent in INPUT or INPUT-OUTPUT mode.
* @param output
* Flag indicating this table is sent in OUTPUT or INPUT-OUTPUT mode.
* @param append
* Flag indicating this table is sent in APPEND mode.
*/
public TableResultSet(boolean input, boolean output, boolean append)
{
super(input, output, append);
}
/**
* Used in conjunction with {@link #nextProperty()}, it provides a mechanism for subclasses
* to send the property list in an iterator-like form.
* <p>
* Override this method so that it returns <code>false</code> when {@link #nextProperty} has
* sent all properties.
* <p>
* WARNING: this allows only one iteration over the properties and is for internal usage
* only. Use {@link #asResultSet()} and the associated {@link ResultSetMetaData} instead.
*
* @return Default it returns <code>false</code>.
*/
public abstract boolean hasMoreProperties();
/**
* Used in conjunction with {@link #hasMoreProperties()}, it provides a mechanism for
* subclasses to send the property list in an iterator-like form.
* <p>
* Override this method so that when {@link #hasMoreProperties} returns <code>true</code>, this
* will return the name of the next property in the list.
* <p>
* WARNING: this allows only one iteration over the properties and is for internal usage
* only. Use {@link #asResultSet()} and the associated {@link ResultSetMetaData} instead.
*
* @return Default it returns <code>null</code>.
*/
public abstract PropertyDefinition nextProperty();
/**
* Used in conjunction with {@link #hasMoreRows()}, it provides a mechanism for subclasses to
* send the data in an iterator-like form.
* <p>
* Override this method so that it returns <code>false</code> when {@link #nextRow} has sent
* all data.
* <p>
* WARNING: this allows only one iteration over the properties and is for internal usage
* only. Use {@link #asResultSet()} instead.
*
* @return Default it returns <code>false</code>.
*/
public abstract boolean hasMoreRows();
/**
* Used in conjunction with {@link #hasMoreRows()}, it provides a mechanism for subclasses to
* send the data in an iterator-like form.
* <p>
* Override this method so that when {@link #hasMoreRows} returns <code>true</code>, this will
* return the next row in the table.
* <p>
* WARNING: this allows only one iteration over the properties and is for internal usage
* only. Use {@link #asResultSet()} and instead.
*
* @return Default it returns <code>null</code>.
*/
public abstract Object[] nextRow();
/**
* Used in conjunction with {@link #hasMoreRows()}, it provides a mechanism for subclasses to
* send the row meta information in an iterator-like form.
* <p>
* Override this method so that when {@link #hasMoreRows} returns <code>true</code>, this will
* return the next row in the table.
* <p>
* WARNING: this allows only one iteration over the properties and is for internal usage
* only. Use {@link #asResultSet()} and instead.
*
* @return Default it returns <code>null</code>.
*/
public abstract Object[] nextRowMeta();
/**
* Set the data sent by a remote side, after the remote appserver call.
*
* @param rows
* The data sent by the remote side.
*/
public void setRows(List<Object[]> rows)
{
this.rows = rows;
}
/**
* Set the meta row information sent by a remote side, after the remote appserver call.
*
* @param rowsMeta
* The meta information sent by the remote side.
*/
public void setRowsMeta(List<Object[]> rowsMeta)
{
this.rowsMeta = rowsMeta;
}
/**
* Get the data sent by a remote side, after the remote appserver call. This will be non-null
* only if the parameter is in OUTPUT or INPUT-OUTPUT mode.
*
* @return See above.
*/
public List<Object[]> getRows()
{
return rows;
}
/**
* Get the row meta information sent by a remote side, after the remote appserver call.
* This will be non-null only if the parameter is in OUTPUT or INPUT-OUTPUT mode.
*
* @return See above.
*/
public List<Object[]> getRowsMeta()
{
return rowsMeta;
}
/**
* Set the metadata sent by a remote side, after the remote appserver call.
*
* @param properties
* The metadata sent by the remote side.
*/
public void setProperties(List<PropertyDefinition> properties)
{
this.properties = properties;
}
/**
* Get the metadata sent by a remote side, after the remote appserver call. This will be
* non-null only if the parameter is in OUTPUT or INPUT-OUTPUT mode.
*
* @return See above.
*/
public List<PropertyDefinition> getProperties()
{
return properties;
}
/**
* Get the data sent by a remote side, after the remote appserver call, as a {@link ResultSet}
* instance. This will be non-null only if the parameter is in OUTPUT or INPUT-OUTPUT mode.
*
* @return See above.
*/
public ResultSet asResultSet()
{
if (!isOutput())
{
return null;
}
Class<?>[] interfaces = { ResultSet.class };
ResultSet proxy = (ResultSet) Proxy.newProxyInstance(getClass().getClassLoader(),
interfaces,
new DataHandler());
return proxy;
}
/**
* Delete row data.
*/
public void clearRows()
{
rows = null;
rowsMeta = null;
}
/**
* Handler for the {@link ResultSetMetaData} proxy.
* Only some of the methods are supported: ({@link ResultSetMetaData#getColumnCount},
* {@link ResultSetMetaData#getColumnName}, {@link ResultSetMetaData#getColumnTypeName} and
* {@link ResultSetMetaData#getColumnType}. Note that, when working with dynamically generated
* tables, internal generic indexed names are used for columns. To obtain the expected column
* name, the column's legacy name is returned instead by <code>getColumnName()</code>.
*/
private final class MetaDataHandler
implements InvocationHandler
{
/** The property list. */
private final List<PropertyDefinition> props;
/**
* Build a new instance which extracts metadata from the given property list.
*
* @param props
* The property list.
*/
public MetaDataHandler(List<PropertyDefinition> props)
{
this.props = props;
}
/**
* Handler for {@link ResultSetMetaData} APIs. Only the following are supported:
* <ul>
* <li>{@link ResultSetMetaData#getColumnCount} - Returns the number of columns in this
* ResultSet object.</li>
* <li>{@link ResultSetMetaData#getColumnName} - Get the designated column's name.<br>
* <strong>Note:</strong>For the in-memory tables that are dynamicaly generated,
* this will return the legacy name not actual column name from the database.
* Since the record structure built dynamicaly, the fields names are generic and
* the returned name is obtained by mapping the legacy name.</li>
* <li>{@link ResultSetMetaData#getColumnTypeName} - Retrieves the designated column's
* database-specific type name.</li>
* <li>{@link ResultSetMetaData#getColumnType} - Retrieves the designated column's SQL
* type.</li>
* </ul>
*/
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable
{
String mname = method.getName();
if ("getColumnCount".equals(mname))
{
return props.size();
}
if (mname.startsWith("get") && args.length == 1 && args[0] instanceof Integer)
{
int idx = (Integer) args[0] - 1;
PropertyDefinition pd = props.get(idx);
if ("getColumnName".equals(mname))
{
return pd.getLegacyName(); // use legacy name instead of internal pd.getName();
}
if ("getColumnTypeName".equals(mname))
{
return pd.getType().getName();
}
if ("getColumnType".equals(mname))
{
// TODO: investigate this: Class.forName("util.pack" + type.lowercase())?
FqlType type = DataTypeHelper.getTypeClass(pd.getType());
return type.getSqlType();
}
}
throw new SQLFeatureNotSupportedException("Method " + mname + " is not supported!");
}
}
/**
* Handler for the {@link ResultSet} proxy. This will provide the data in a
* {@link ResultSet#TYPE_FORWARD_ONLY forward-only} manner.
*/
private final class DataHandler
implements InvocationHandler
{
/** The {@link ResultSetMetaData} proxy. */
private ResultSetMetaData rsmd;
/** The list of properties. */
private List<PropertyDefinition> props;
/** An iterator over the data. */
private Iterator<Object[]> iter;
/** Flag indicating if the last retrieved column was SQL-null. */
private boolean wasNull = false;
/** The data for the current row. */
private Object[] data = null;
/**
* Create a new instance. This initializes the iterator, builds the
* {@link #props property list} and the {@link #rsmd metadata}.
*/
public DataHandler()
{
iter = rows.iterator();
props = new ArrayList<>(properties);
Class<?>[] interfaces = { ResultSetMetaData.class };
rsmd = (ResultSetMetaData) Proxy.newProxyInstance(getClass().getClassLoader(),
interfaces,
new MetaDataHandler(props));
}
/**
* Handler for the {@link ResultSet} APIs. Only the following are supported:
* <ul>
* <li>{@link ResultSet#next}</li>
* <li>{@link ResultSet#close}</li>
* <li>{@link ResultSet#isClosed}</li>
* <li>{@link ResultSet#wasNull}</li>
* <li>{@link ResultSet#findColumn}</li>
* <li>{@link ResultSet#relative}</li>
* <li>{@link ResultSet#getType}</li>
* <li>{@link ResultSet#getMetaData}</li>
* <li>{@link ResultSet#getObject}</li>
* <li>{@link ResultSet#getBoolean}</li>
* <li>{@link ResultSet#getDouble}</li>
* <li>{@link ResultSet#getBigDecimal}</li>
* <li>{@link ResultSet#getFloat}</li>
* <li>{@link ResultSet#getInt}</li>
* <li>{@link ResultSet#getLong}</li>
* <li>{@link ResultSet#getShort}</li>
* <li>{@link ResultSet#getByte}</li>
* <li>{@link ResultSet#getString}</li>
* <li>{@link ResultSet#getDate}</li>
* <li>{@link ResultSet#getTime}</li>
* <li>{@link ResultSet#getTimestamp}</li>
* </ul>
*/
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable
{
String mname = method.getName();
if ("close".equals(mname))
{
// discard all data
iter = null;
rsmd = null;
props = null;
return null;
}
if ("isClosed".equals(mname))
{
return (iter == null);
}
if (iter == null)
{
// all next APIs require for the result set to be opened.
throw new SQLException("The ResultSet is closed!");
}
if ("next".equals(mname))
{
boolean next = iter.hasNext();
data = (next ? iter.next() : null);
return next;
}
if ("wasNull".equals(mname))
{
return wasNull;
}
if ("findColumn".equals(mname))
{
return getPropertyIndex(mname);
}
if ("relative".equals(mname))
{
int rel = (Integer) args[0];
if (rel < 0)
{
throw new SQLFeatureNotSupportedException("The result set is forward only!");
}
else
{
for (int i = 0; i < rel; i++)
{
if (iter.hasNext())
{
data = iter.next();
}
else
{
// passed last record
data = null;
return false;
}
}
}
return true;
}
if ("getType".equals(mname))
{
return ResultSet.TYPE_FORWARD_ONLY;
}
if ("getMetaData".equals(mname))
{
return rsmd;
}
Object res = null;
if (mname.startsWith("get"))
{
if (data == null)
{
throw new SQLException("Invalid cursor! next() was not called or the last record" +
"was reached.");
}
if (data.length == 0)
{
throw new SQLException("The current record has no fields!");
}
wasNull = false;
int idx = 0;
if (args[0] instanceof String)
{
idx = getPropertyIndex((String) args[0]);
}
else
{
idx = (Integer) args[0];
}
// make this 0-based index
idx = idx - 1;
if (idx >= data.length || idx < 0)
{
final String msg = "Invalid column index: %d; valid indexes are 1 to %d";
throw new SQLException(String.format(msg, idx + 1, data.length));
}
Object val = data[idx];
if (val == null)
{
LOG.warning("Received NULL value for field at index " + (idx + 1) +
" when invoking " + mname + ".");
}
boolean resolved = true;
boolean isNull = (val == null ||
(val instanceof BaseDataType && ((BaseDataType) val).isUnknown()));
// the remote side always sends P2J wrapper types - do not resolve other types
if ("getObject".equals(mname))
{
if (args.length != 1)
{
throw new SQLException("getObject supports only one argument.");
}
if (isNull)
{
// this API allows NULL values.
res = null;
}
else
{
int type = rsmd.getColumnType(idx + 1);
switch (type)
{
case Types.VARCHAR:
res = ((BaseDataType) val).toStringMessage();
break;
case Types.DATE:
res = new java.sql.Date(((date) val).dateValue().getTime()); // TODO: what if [val] is unknown?
break;
case Types.TIMESTAMP:
res = new java.sql.Timestamp(((date) val).dateValue().getTime()); // TODO: what if [val] is unknown?
break;
case Types.NUMERIC:
res = new BigDecimal(((decimal) val).toStringMessage());
break;
case Types.BIGINT:
res = (val instanceof int64 ? ((int64) val).toJavaLongType()
: ((rowid) val).getValue());
break;
case Types.INTEGER:
res = ((integer) val).toJavaIntegerType();
break;
case Types.BIT:
res = ((logical) val).toJavaType();
break;
case Types.VARBINARY:
res = ((raw) val).getByteArray();
break;
default:
resolved = false;
break;
}
}
}
// start of APIs which can return null
else if ("getString".equals(mname))
{
if (isNull)
{
res = null;
}
else if (val instanceof BaseDataType)
{
res = ((BaseDataType) val).toStringMessage();
}
else
{
resolved = false;
}
}
else if ("getBytes".equals(mname))
{
if (isNull)
{
res = null;
}
else if (val instanceof BinaryData)
{
res = ((BinaryData) val).getByteArray();
}
else
{
resolved = false;
}
}
else if ("getBigDecimal".equals(mname))
{
if (isNull)
{
res = null;
}
else if (val instanceof NumberType)
{
res = new BigDecimal(((NumberType) val).doubleValue());
if (args.length == 2)
{
int scale = (Integer) args[1];
res = ((BigDecimal) res).setScale(scale);
}
}
else
{
resolved = false;
}
}
else if ("getDate".equals(mname))
{
// TODO: Calendar parameter?
if (isNull)
{
res = null;
}
else if (val instanceof date)
{
res = new java.sql.Date(((date) val).dateValue().getTime()); // TODO: what if [val] is unknown?
}
else
{
resolved = false;
}
}
else if ("getTime".equals(mname))
{
// TODO: Calendar parameter?
if (isNull)
{
res = null;
}
else if (val instanceof date)
{
res = new java.sql.Time(((date) val).dateValue().getTime()); // TODO: what if [val] is unknown?
}
else
{
resolved = false;
}
}
else if ("getTimestamp".equals(mname))
{
// TODO: Calendar parameter?
if (isNull)
{
res = null;
}
else if (val instanceof date)
{
res = new java.sql.Timestamp(((date) val).dateValue().getTime()); // TODO: what if [val] is unknown?
}
else
{
resolved = false;
}
}
// start of APIs which return a non-null value
else if (isNull)
{
// do not allow NULLs from here on
throw new NullPointerException("Value for column " + args[0].toString() +
" is null or unknown, when calling " + mname + "!");
}
else if ("getBoolean".equals(mname))
{
if (val instanceof logical)
{
res = ((logical) val).booleanValue();
}
else
{
resolved = false;
}
}
else if ("getDouble".equals(mname))
{
if (val instanceof NumberType)
{
res = ((NumberType) val).doubleValue();
}
else
{
resolved = false;
}
}
else if ("getFloat".equals(mname))
{
if (val instanceof NumberType)
{
res = (float) ((NumberType) val).doubleValue();
}
else
{
resolved = false;
}
}
else if ("getInt".equals(mname))
{
if (val instanceof NumberType)
{
res = ((NumberType) val).intValue();
}
else
{
resolved = false;
}
}
else if ("getLong".equals(mname))
{
if (val instanceof NumberType)
{
res = ((NumberType) val).longValue();
}
else
{
resolved = false;
}
}
else if ("getShort".equals(mname))
{
if (val instanceof NumberType)
{
res = (short) ((NumberType) val).intValue();
}
else
{
resolved = false;
}
}
else if ("getByte".equals(mname))
{
if (val instanceof NumberType)
{
res = (byte) ((NumberType) val).intValue();
}
else
{
resolved = false;
}
}
if (!resolved)
{
throw new SQLException("Value '" + (val == null ? "null" : val.toString()) + "'" +
" for column " + args[0].toString() +
"[" + rsmd.getColumnType(idx + 1) + "] is not compatible with method " +
mname + "!");
}
else
{
wasNull = (res != null);
}
return res;
}
throw new SQLFeatureNotSupportedException("Method " + mname + " is not supported!");
}
/**
* Get the 1-based index for the property with the specified name. Search is done
* case-insensitive. If not found, an {@link SQLException} is throw.
*
* @param name
* The property name.
*
* @return The 1-based index in the property list.
*
* @throws SQLException
* If the property was not found.
*/
private int getPropertyIndex(String name)
throws SQLException
{
for (int i = 0; i < props.size(); i++)
{
PropertyDefinition pd = props.get(i);
if (pd.getName().equalsIgnoreCase(name))
{
return i + 1;
}
}
throw new SQLException("Property " + name + " was not found!");
}
}
}