DataTypeHelper.java
/*
** Module : DataTypeHelper.java
** Abstract : helper to manage data types of objects referenced by HQL
**
** Copyright (c) 2004-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
** 001 ECF 20080310 @37492 Created initial version. Provides static helper methods to determine
** the data types of various constructs found within HQL.
** 002 ECF 20080609 @38681 Fixed initBasicTokens(). Added STRING/TYPE constant mapping to
** tokens map.
** 003 ECF 20080804 @39310 Added rowid support. Map Long and rowid classes, and long primitive,
** to HQLTypes.LONG enum.
** 004 SVL 20090225 @41366 Added recid support (mapped to HQLTypes.INTEGER).
** 005 GES 20090518 @42391 Import change.
** 006 ECF 20090817 @43683 Added persistence *Field types to classes map.
** 007 SVL 20120331 Upgraded to Hibernate 4.
** 008 OM 20130501 Added support for 64 bit data types.
** 009 OM 20130527 Added mapping for Timestamp.class as HQLTypes.DATETIME and
** Object[] for HQLTypes.DATETIMETZ (don't know if this is correct).
** For the moment this is disabled (1584#150).
** 010 VMN 20141127 Renamed DatetimeTzField.class to DatetimetzField.class.
** 011 CA 20151025 Registered the handle.class type.
** 012 OM 20151130 Added HQLFunction support in initFunction() method.
** 013 ECF 20160114 Removed references to obsolete *Field classes.
** 014 ECF 20160226 Changes required by new PropertyHelper implementation.
** 015 CA 20160226 In a ternary, if 'then' expression is unknown, then resolve the
** datatype from the 'else' expresson.
** 016 ECF 20160720 Fixed buffer lookup during HQL preprocessing.
** 017 CA 20180517 Added comhandle data type.
** 018 OM 20190202 Added support for object data type.
** 019 CA 20190722 Fixed datetimetz query arguments - they are treated like a ISO date,
** in string representation. UDF are used to compare them.
** 020 CA 20200610 Initialize this class, as can be used outside of the FWD environment, for
** appserver clients.
** 021 CA 20200927 Use IdentityHashMap instead of plain map when the key is a Class.
** 022 IAS 20201123 Added ARRAY type
** IAS 20220926 Process extra parentheses in the expressionType() method.
** OM 20221103 New class names for FQLPreprocessor, FQLExpression, FQLBundle, and FQLCache.
** 023 AL2 20230210 Allow permissive look-up buffer (without exception of fail).
** 024 CA 20240331 Use logical constants for TRUE, FALSE, UNKNOWN, within internal FWD runtime, so
** logical type checks must include sub-classes, too.
** 025 DDF 20240410 Added ValueTimestampTimeZone H2 type for FqlType.DATETIMETZ when
** initializing classes.
** 026 SP 20240516 Process the function name in getTypeFunction() when there is an underscore
** 027 CA 20241107 Added character constant support.
** 028 ICP 20250123 Added support for integer and int64 constants.
*/
/*
** 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.hql;
import java.lang.reflect.*;
import java.math.*;
import java.sql.*;
import java.sql.Date;
import java.sql.Timestamp;
import java.util.*;
import com.goldencode.ast.*;
import com.goldencode.p2j.persist.*;
import com.goldencode.p2j.persist.pl.*;
import com.goldencode.p2j.util.*;
import org.h2.api.*;
import org.h2.value.ValueTimestampTimeZone;
/**
* A helper to manage data types of objects referenced by FQL statements
* (specifically, where clauses). This class provides static helper methods
* to determine the data type of:
* <ul>
* <li>certain non-volatile tokens (i.e., constants or other token types
* whose data types cannot change based on other factors)
* <li>standard functions, such as <code>upper()</code>
* <li>user defined functions, such as all the built-in functions migrated
* from Progress
* <li>query substitution parameters
* <li>qualified and unqualified references to DMO properties
* </ul>
* <p>
* The {@link FqlType} enum is used to rationalize all the various type
* constructs (token types, Java class types, Hibernate types) into a common
* construct. Any request for type information will be in terms of an
* <code>HQLTypes</code> enum value.
*/
public final class DataTypeHelper
implements HQLParserTokenTypes
{
/** Map of token types to HQL types */
private static final Map<Integer, FqlType> tokens = new HashMap<>();
/** Map of functions to HQL types */
private static final Map<FunctionKey, FqlType> functions = new HashMap<>();
/** Map of Java class types to HQL types */
private static final Map<Class<?>, FqlType> classes = new IdentityHashMap<>();
/** Map of SQL types to BDT classes */
private static final Map<Integer, Class<? extends BaseDataType>> bdts = new HashMap<>();
static
{
initialize();
}
/**
* Private constructor; access is via static methods.
*/
private DataTypeHelper()
{
}
/**
* Initialize the data structures needed to enable this helper. This
* should be invoked early during initialization of the persistence
* runtime.
*/
public static void initialize()
{
// Initialize Java data type wrapper mappings.
initJavaClasses();
// Initialize basic token type mappings.
initBasicTokens();
// Initialize standard built-in function return type mappings.
initFunctions();
}
/**
* Get the FQL type associated with a particular Java class.
*
* @param javaType
* A Java class.
*
* @return The FQL type associated with the given class, or {@link
* FqlType#UNKNOWN} if no mapping is available.
*/
public static FqlType getTypeClass(Class<?> javaType)
{
FqlType type = classes.get(javaType);
return (type != null) ? type : FqlType.UNKNOWN;
}
public static Class<? extends BaseDataType> getBdtClass(int sqlType)
{
return bdts.get(sqlType);
}
/**
* Get the FQL type associated with a particular token type. Only those
* token types whose data type cannot vary (i.e., constants and certain
* operators) are handled by this method.
*
* @param tokenType
* A token type from {@link HQLParserTokenTypes}.
*
* @return The FQL type associated with the given token type, or {@link
* FqlType#UNKNOWN} if no mapping is available.
*/
public static FqlType getTypeToken(int tokenType)
{
FqlType type = tokens.get(tokenType);
return (type != null) ? type : FqlType.UNKNOWN;
}
/**
* Get the FQL type associated with a particular {@link FunctionKey}.
*
* @param key
* A <code>FunctionKey</code> which describes a standard function
* or a built-in function.
*
* @return The FQL type associated with the given function key, or {@link
* FqlType#UNKNOWN} if no mapping is available.
*/
public static FqlType getTypeFunction(FunctionKey key)
{
FqlType type = functions.get(key);
return (type != null) ? type : FqlType.UNKNOWN;
}
/**
* Get the FQL type associated with a particular, qualified DMO property,
* as represented by the given AST.
*
* @param aliasAst
* An AST which represents a DMO alias, whose first (and only)
* child represents a property of that DMO.
* @param bufferMap
* A map of DMO aliases to their corresponding record buffers, where {@code null}
* is mapped to the buffer whose property references in the FQL expression are
* unqualified (if any).
*
* @return The FQL type associated with the given property, or {@link
* FqlType#UNKNOWN} if no mapping is available.
*
* @throws PersistenceException
* if the property does not exist, or if there is some error
* determining its data type.
*/
public static FqlType getTypeQualifiedProperty(Aast aliasAst,
Map<String, RecordBuffer> bufferMap)
throws PersistenceException
{
return getTypeClass(getJavaTypeQualifiedProperty(aliasAst, bufferMap));
}
/**
* Get the FQL type associated with a particular, unqualified DMO property,
* as represented by the given AST.
*
* @param propAst
* An AST which represents a property of a DMO.
* @param bufferMap
* A map of DMO aliases to their corresponding record buffers, where {@code null}
* is mapped to the buffer whose property references in the FQL expression are
* unqualified (if any).
*
* @return The FQL type associated with the given property, or {@link
* FqlType#UNKNOWN} if no mapping is available.
*
* @throws PersistenceException
* if the property does not exist, or if there is some error
* determining its data type.
*/
public static FqlType getTypeUnqualifiedProperty(Aast propAst,
Map<String, RecordBuffer> bufferMap)
throws PersistenceException
{
return getTypeClass(getJavaTypeUnqualifiedProperty(propAst, bufferMap));
}
/**
* Get the Java type associated with a particular, qualified DMO property, as represented by the given AST.
*
* @param aliasAst
* An AST which represents a DMO alias, whose first (and only)
* child represents a property of that DMO.
* @param bufferMap
* A map of DMO aliases to their corresponding record buffers, where {@code null} is mapped to the
* buffer whose property references in the FQL expression are unqualified (if any).
*
* @return The Java type associated with the given property.
*
* @throws PersistenceException
* if the property does not exist, or if there is some error
* determining its data type.
*/
public static Class<?> getJavaTypeQualifiedProperty(Aast aliasAst, Map<String, RecordBuffer> bufferMap)
throws PersistenceException
{
Aast propAst = aliasAst.getChildAt(0);
String property = propAst.getText();
RecordBuffer buffer = lookupBuffer(aliasAst, bufferMap, null);
Class<? extends DataModelObject> dmoIface = buffer.getDMOInterface();
Class<?> javaType = DBUtils.getDMOPropertyType(dmoIface, property);
return javaType;
}
/**
* Get the Java type associated with a particular, unqualified DMO property, as represented by the given AST.
*
* @param propAst
* An AST which represents a property of a DMO.
* @param bufferMap
* A map of DMO aliases to their corresponding record buffers, where {@code null} is mapped to the
* buffer whose property references in the FQL expression are unqualified (if any).
*
* @return The Java type associated with the given property.
*
* @throws PersistenceException
* if the property does not exist, or if there is some error
* determining its data type.
*/
public static Class<?> getJavaTypeUnqualifiedProperty(Aast propAst, Map<String, RecordBuffer> bufferMap)
throws PersistenceException
{
Aast parent = propAst.getParent();
Aast aliasAst = parent != null && parent.getType() == ALIAS ? parent : null;
RecordBuffer buffer = lookupBuffer(aliasAst, bufferMap, null);
Class<? extends DataModelObject> dmoIface = buffer.getDMOInterface();
String property = propAst.getText();
Class<?> javaType = DBUtils.getDMOPropertyType(dmoIface, property);
return javaType;
}
/**
* Determine the FQL type of the expression represented by the given AST node, which may be
* a single leaf node, or the head of a more complex expression.
*
* @param node
* AST node whose FQL type is to be determined.
* @param bufferMap
* A map of DMO aliases to their corresponding record buffers, where {@code null}
* is mapped to the buffer whose property references in the FQL expression are
* unqualified (if any).
*
* @return The FQL type which represents the data type to which the given
* expression would evaluate.
*
* @throws PersistenceException
* if a DMO property referenced within the expression does not
* exist, or if there is some error determining its data type.
*/
public static FqlType expressionType(Aast node, Map<String, RecordBuffer> bufferMap)
throws PersistenceException
{
FqlType c0;
FqlType c1;
int tokType = node.getType();
switch (tokType)
{
case ALIAS:
return getTypeQualifiedProperty(node, bufferMap);
case PROPERTY:
return getTypeUnqualifiedProperty(node, bufferMap);
case FUNCTION:
return getTypeFunction(node, bufferMap);
case SUBST:
return FqlType.valueOf((String) node.getAnnotation("datatype"));
case TERNARY:
c0 = expressionType(node.getChildAt(1), bufferMap); // case...then
c1 = expressionType(node.getChildAt(2), bufferMap); // case...else
if (c0 == FqlType.DECIMAL || c1 == FqlType.DECIMAL)
{
return FqlType.DECIMAL;
}
return (c0 == FqlType.LONG || c1 == FqlType.LONG)
? FqlType.LONG
: (c0 == FqlType.UNKNOWN ? c1 : c0);
case PLUS:
case MINUS:
case MULTIPLY:
case DIVIDE:
c0 = expressionType(node.getChildAt(0), bufferMap); // lvalue
c1 = expressionType(node.getChildAt(1), bufferMap); // rvalue
if (c0 == FqlType.DECIMAL || c1 == FqlType.DECIMAL)
{
return FqlType.DECIMAL;
}
return (c0 == FqlType.LONG || c1 == FqlType.LONG)
? FqlType.LONG
: FqlType.INTEGER;
case UN_MINUS:
return expressionType(node.getChildAt(0), bufferMap);
case LPARENS:
if (node.getNumImmediateChildren() == 1)
{
return expressionType((Aast)node.getFirstChild(), bufferMap);
}
// fall-through if none or more than one childs
default:
return getTypeToken(tokType);
}
}
/**
* Attempt to look up a record buffer by alias from the given map. The DMO alias is extracted
* from the {@code alias} AST. If there is no matching entry in the buffer map, an exception
* is thrown.
* <p>
* The returned buffer is initialized by side effect.
*
* @param alias
* DMO alias AST. If {@code null}, a {@code null} lookup key is used.
* @param bufferMap
* A map of DMO aliases to record buffers. If no alias is provided (e.g., for an
* unqualified property reference, the appropriate buffer is expected to be mapped
* to the {@code null} key.
* @param fql
* Optional FQL expression from which the alias is parsed.
*
* @return A record buffer matching the given alias.
*/
public static RecordBuffer lookupBuffer(Aast alias,
Map<String, RecordBuffer> bufferMap,
FQLExpression fql)
{
String aliasName = alias != null ? alias.getText() : null;
RecordBuffer rb = lookupBuffer(aliasName, bufferMap, fql);
if (rb == null)
{
throw new IllegalArgumentException("Unrecognized buffer at column " +
(alias != null ? alias.getColumn() : "<unknown>") +
" in where clause: '" +
aliasName +
"' [" +
(fql != null ? fql.toString() : "<unavailable>") +
"]"
);
};
return rb;
}
/**
* Attempt to look up a record buffer by alias from the given map. The DMO alias is extracted
* from the {@code alias} AST. If there is no matching entry in the buffer map, an exception
* is thrown.
* <p>
* The returned buffer is initialized by side effect.
*
* @param aliasName
* Alias for DMO. If {@code null}, a {@code null} lookup key is used.
* @param bufferMap
* A map of DMO aliases to record buffers. If no alias is provided (e.g., for an
* unqualified property reference, the appropriate buffer is expected to be mapped
* to the {@code null} key.
* @param fql
* Optional FQL expression from which the alias is parsed.
*
* @return A record buffer matching the given alias.
*/
public static RecordBuffer lookupBuffer(String aliasName,
Map<String, RecordBuffer> bufferMap,
FQLExpression fql)
{
RecordBuffer buffer = bufferMap.get(aliasName);
if (buffer == null)
{
return null;
}
buffer.initialize();
return buffer;
}
/**
* Get the FQL type which will be returned when the given function is executed.
*
* @param function
* AST node representing a standard or built-in function expression.
* @param bufferMap
* A map of DMO aliases to their corresponding record buffers, where {@code null}
* is mapped to the buffer whose property references in the FQL expression are
* unqualified (if any).
*
* @return The FQL type associated with the given function, or {@link FqlType#UNKNOWN} if
* no mapping is available.
*
* @throws PersistenceException
* if a DMO property referenced within the expression does not exist, or if there
* is some error determining its data type.
*/
private static FqlType getTypeFunction(Aast function, Map<String, RecordBuffer> bufferMap)
throws PersistenceException
{
String name = function.getText();
int k = name.indexOf('_');
if (k > 0)
{
name = name.substring(0, k);
}
int len = function.getNumImmediateChildren();
FqlType[] fqlSig = new FqlType[len];
int i = 0;
Aast child = (Aast) function.getFirstChild();
while (child != null)
{
fqlSig[i++] = expressionType(child, bufferMap);
child = (Aast) child.getNextSibling();
}
FunctionKey key = new FunctionKey(name, fqlSig);
return getTypeFunction(key);
}
/**
* Initialize the mapping of Java classes to {@link FqlType} for those
* classes which are known to be used in FQL where clauses.
*/
private static void initJavaClasses()
{
// Java primitive types.
classes.put(Boolean.TYPE , FqlType.BOOLEAN);
classes.put(Integer.TYPE , FqlType.INTEGER);
classes.put(Long.TYPE , FqlType.LONG);
classes.put(Double.TYPE , FqlType.DOUBLE);
// Standard Java wrapper types.
classes.put(String.class , FqlType.TEXT);
classes.put(Boolean.class , FqlType.BOOLEAN);
classes.put(Integer.class , FqlType.INTEGER);
classes.put(Long.class , FqlType.LONG);
classes.put(BigDecimal.class , FqlType.DECIMAL);
classes.put(Date.class , FqlType.DATE);
classes.put(Timestamp.class , FqlType.DATETIME);
classes.put(TimestampWithTimeZone.class, FqlType.DATETIMETZ);
classes.put(ValueTimestampTimeZone.class, FqlType.DATETIMETZ);
classes.put(String[].class , FqlType.ARRAY);
//classes.put(Object[].class , FqlTypes.DATETIMETZ); // datetimetz is represented by a 2-uple (Timestamp, Offset)
// P2J wrapper types.
classes.put(logical.class , FqlType.BOOLEAN);
classes.put(integer.class , FqlType.INTEGER);
classes.put(recid.class , FqlType.INTEGER);
classes.put(decimal.class , FqlType.DECIMAL);
classes.put(date.class , FqlType.DATE);
classes.put(raw.class , FqlType.RAW);
classes.put(character.class , FqlType.TEXT);
classes.put(rowid.class , FqlType.LONG);
classes.put(int64.class , FqlType.LONG);
classes.put(datetime.class , FqlType.DATETIME);
classes.put(datetimetz.class , FqlType.DATETIMETZ);
classes.put(handle.class , FqlType.TEXT);
classes.put(comhandle.class , FqlType.TEXT);
classes.put(object.class , FqlType.TEXT);
classes.put(ObjectVar.class , FqlType.TEXT);
// BDT constants
classes.put(logical.logicalConstant.class, FqlType.BOOLEAN);
classes.put(character.characterConstant.class, FqlType.TEXT);
classes.put(integer.integerConstant.class, FqlType.INTEGER);
classes.put(int64.int64Constant.class, FqlType.LONG);
bdts.put(Types.BOOLEAN, logical.class);
bdts.put(Types.INTEGER, integer.class);
// bdts.put(Types.?, recid.class);
bdts.put(Types.DECIMAL, decimal.class);
bdts.put(Types.DATE, date.class);
// bdts.put(Types.?, raw.class);
bdts.put(Types.VARCHAR, character.class);
// bdts.put(Types.?, rowid.class);
bdts.put(Types.BIGINT, int64.class);
bdts.put(Types.TIMESTAMP, datetime.class);
bdts.put(Types.TIMESTAMP_WITH_TIMEZONE, datetimetz.class);
// bdts.put(Types.?, handle.class);
// bdts.put(Types.?, comhandle.class);
// bdts.put(Types.?, object.class);
// bdts.put(Types.?, ObjectVar.class);
}
/**
* Initialize the mapping of basic token types to {@link FqlType}. Only
* those token types whose data types will not change (i.e., constants and
* certain operators) are mapped here.
*/
private static void initBasicTokens()
{
tokens.put(BOOL_TRUE , FqlType.BOOLEAN);
tokens.put(BOOL_FALSE , FqlType.BOOLEAN);
tokens.put(IS_NULL , FqlType.BOOLEAN);
tokens.put(NOT_NULL , FqlType.BOOLEAN);
tokens.put(EQUALS , FqlType.BOOLEAN);
tokens.put(NOT_EQ , FqlType.BOOLEAN);
tokens.put(GT , FqlType.BOOLEAN);
tokens.put(LT , FqlType.BOOLEAN);
tokens.put(LTE , FqlType.BOOLEAN);
tokens.put(GTE , FqlType.BOOLEAN);
tokens.put(AND , FqlType.BOOLEAN);
tokens.put(OR , FqlType.BOOLEAN);
tokens.put(NOT , FqlType.BOOLEAN);
tokens.put(NUM_LITERAL , FqlType.INTEGER);
tokens.put(LONG_LITERAL, FqlType.LONG);
tokens.put(DEC_LITERAL , FqlType.DECIMAL);
tokens.put(STRING , FqlType.TEXT);
}
/**
* Initialize the mappings of {@link FunctionKey} to {@link FqlType} for
* all standard functions used in FQL (including built-in functions which
* emulate Progress functionality).
*/
private static void initFunctions()
{
initFunction("upper", FqlType.TEXT, FqlType.TEXT);
for (Method method : BuiltIns.getStandardFunctions())
{
initFunction(method);
}
}
/**
* Given a Java method which represents a user-defined function accessible
* from FQL, create a mapping of {@link FunctionKey} to {@link FqlType} for that function.
*
* @param method
* Java method to be mapped.
*/
private static void initFunction(Method method)
{
HQLFunction hqlFunction = method.getAnnotation(HQLFunction.class);
if (hqlFunction == null)
{
throw new IllegalArgumentException(
"Method " + method + " is not backing a hql function");
}
String name = hqlFunction.name();
// use the annotation to obtain the correct function name generated in the code
if (name.isEmpty())
{
name = method.getName();
// if not explicitly set, it matches the name of method
}
Class<?> returns = method.getReturnType();
FqlType hqlReturns = classes.get(returns);
if (hqlReturns == null)
{
throw new IllegalArgumentException(
"No HQL type found for user defined function return type " + returns.getName());
}
Class<?>[] sig = method.getParameterTypes();
FqlType[] hqlSig = new FqlType[sig.length];
for (int i = 0; i < sig.length; i++)
{
Class<?> next = sig[i];
FqlType hqlParam = classes.get(next);
if (hqlParam == null)
{
throw new IllegalArgumentException(
"No HQL type found for user defined function parameter type " + next.getName());
}
hqlSig[i] = hqlParam;
}
FunctionKey key = new FunctionKey(name, hqlSig);
functions.put(key, hqlReturns);
}
/**
* Given the name and signature of a user-defined function accessible from
* HQL, create a mapping of {@link FunctionKey} to {@link FqlType} for that function.
*
* @param name
* Function name
* @param returns
* Data type that the function returns.
* @param paramTypes
* Data types of function parameters, if any.
*/
private static void initFunction(String name, FqlType returns, FqlType... paramTypes)
{
FunctionKey key = new FunctionKey(name, paramTypes);
functions.put(key, returns);
}
}