DBUtils.java
/*
** Module : DBUtils.java
** Abstract : Static helper methods for DB services
**
** Copyright (c) 2004-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
** 001 ECF 20060712 @28043 Created initial version. Static utility methods for the persistence
** framework.
** 002 ECF 20060714 @28136 Made class and most methods public. Access is required from the
** schema package for import.
** 003 ECF 20070416 @33031 Added handleInterrupt(). Inspects an exception and throws
** StopConditionException if it finds InterruptedException within the
** exception's causal chain.
** 004 EVL 20070609 @34005 Adding explicit import of the class org.hibernate.type.Type to
** eliminate conflict with the same class from java.lang.reflect
** package to be able to compile for Java 6.
** 005 ECF 20070705 @34358 Enhanced makeTypeArray(). Entity parameters are now allowed.
** 006 ECF 20071207 @36260 Changed findDMOGetterMethod() signature. Now
** throws PersistenceException rather than IllegalArgumentException.
** 007 ECF 20080310 @37469 Added new database dialect support. Method composePropertyName()
** now supports computed columns. Also added new variant of method
** getDMOPropertyType().
** 008 ECF 20080318 @37542 Fixed makeTypeArray(). It was not properly handling unresolved
** (i.e. Resolvable) query substitution parameters.
** 009 ECF 20080330 @37731 Fixed composePropertyName() (package private variant). It was
** qualifying property name with an alias at the wrong point.
** 010 ECF 20080510 @38483 Added new methods. entityForDMOInterface() and dmoClassForEntity().
** 011 ECF 20080806 @39318 Added rowid support. New mappings for 64-bit integers. Import
** persist.type package, since UserType classes moved.
** 012 CA 20080815 @39454 Changed handleInterrupt to handleException.
** The method will handle database connection errors beside interrupt.
** 013 GES 20090424 @41936 Import change.
** 014 SVL 20090225 @41365 Into TYPES map added mapping for recid.
** 015 ECF 20090525 @42972 Added composePropertyName() variant. Added dirty parameter to
** differentiate between primary and dirty database dialects when
** composing property names.
** 016 ECF 20090717 @43228 Created workaround for remote dirty database lookup failure.
** We drop back to using primary database's dialect in
** composePropertyName().
** 017 ECF 20090728 @43416 Fixed getDMOPropertyType(). RecordBuffer change
** had regressed this method.
** 018 ECF 20090817 @43682 Added new *Field types to TYPES map.
** 019 VMN 20130416 Added method getGetterSetterType().
** 020 SVL 20130331 Upgraded to Hibernate 4.
** 021 OM 20130430 Added new int64 and Int64Field to TYPES map.
** 022 OM 20130527 Remapped DatetimeTzUserType as CompositeCustomType.
** For the moment this is disabled (1584#150).
** 023 SVL 20130716 Use system class loader in dmoClassForEntity.
** 024 ECF 20131010 Added metadata support.
** 025 OM 20140410 Fixed support case-sensitive fields in indexes.
** 026 VMN 20140502 Added enhance schema name conversion support for hint "escape"
** attribute. Added helper methods for processing backticks.
** 027 OM 20140513 Added DEFAULT_MAX_INDEX_SIZE & TABLE_ID_FIELD_SIZE constant fields.
** 028 OM 20140623 Added new parameter to composePropertyName for insering the proper
** rtrim sql function based of the dialect used.
** 029 VMN 20141127 Renamed DatetimeTzField.class to DatetimetzField.class.
** 030 ECF 20150220 Replaced Apache commons logging with J2SE logging.
** 031 SVL 20150503 Added getFieldForAddLikeColumn functions.
** 032 ECF 20150815 Added isMandatoryProperty.
** 033 CA 20151025 Registered the handle type.
** 034 ECF 20160114 Removed references to obsolete *Field classes.
** 035 ECF 20160225 Reimplemented certain methods, due to PropertyHelper rewrite.
** 036 OM 20160603 Fixed rtrimming to space character only.
** 037 ECF 20160711 Removed unsafe variant of getDMOPropertyType which could retrieve
** a record buffer in the incorrect buffer scope, given a DMO alias.
** Made notNullPropSets thread-safe.
** 038 OM 20160822 Fixed getDMOPropertyType() to take in consideration computed columns.
** 039 SVL 20161007 Added trimToTableCount().
** 040 ECF 20170901 Remap recid.class to RecIDUserType.
** 041 OM 20171213 Added getSubselectAlias() method.
** 042 CA 20180517 Added comhandle data type.
** 043 OM 20190202 Added support for object data type.
** 044 CA 20190722 Fixed datetimetz query arguments - they are treated like a ISO date,
** in string representation. UDF are used to compare them.
** CA 20190829 Virtual properties (starting with '_') are looked in the
** TempTableRecord interface.
** 045 ECF 20200906 New ORM implementation.
** 046 OM 20201012 Force use locally cached meta information instead of map lookup.
** OM 20210127 Replaced TableMapper.getLegacyName() triple map lookup with direct access to
** local dmoMeta.legacyTable.
** OM 20210309 Do not use DmoMeta as key in TableMapper because temp-tables may share the same
** DmoMeta instance.
** ECF 20210506 Use RecordBuffer.getDmoInfo() getter instead of direct field access to
** prevent NPE in proxy case.
** ECF 20210512 Added logging of database connection error.
** OM 20210628 Access property.name more directly instead of using TableMapper.
** SVL 20210809 Added getMatchingBufferField.
** CA 20210929 getGetterSetterType() now handles the PK, too.
** OM 20220707 Shared meta-knowledge about fields and properties of a record was collected in
** DmoMeta to avoid duplicating their collection and storage.
** 047 HC 20230116 Replaced some handle usages with the actual wrapped resources for
** performance.
** CA 20230116 Avoid using handle.unwrap, handle.getReference or other BDT usage from within FWD
** runtime.
** 048 DDF 20230330 Compose property name with upper and rtrim if dialect doesn't handle
** auto rtrimming and case-insensitive compare operations.
** 049 DDF 20230406 Replaced isAutoRtrimAndCi with isAutoRtrim and isAutoCi. Used a Consumer
** to avoid code duplication.
** 050 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 051 OM 20230630 Added support for the newly observed PARENT_ID_RELATION between joined tables.
** 052 AI 20231122 Used dmoMeta.getFieldInfo() to retrieve field name.
** 053 AI 20231218 Removed the use of getFieldInfo and replaced with dmoMeta.byLegacyName().
** 054 AI 20240228 Updated getFieldForAddLikeColumn to use Property.extent value instead of
** dmoInfo.getExtentMap.
** 055 ICP 20240705 Fixed method of obtaining the class loader.
** 056 OM 20240909 Improved Database API.
** 057 SP 20240916 Fixed makeTypeArray() not handling ParamResolver. Assumed to return a character.
** 058 LS 20250403 Added string constants for error messages.
** Updated handleException to detect jdbc connection error.
** 059 ES 20250424 Handle StopConditionException through ErrorManager.
*/
/*
** 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.util.*;
import java.util.concurrent.*;
import java.util.function.Consumer;
import java.util.logging.*;
import com.goldencode.p2j.*;
import com.goldencode.p2j.classloader.MultiClassLoader;
import com.goldencode.p2j.persist.hql.*;
import com.goldencode.util.*;
import com.goldencode.p2j.persist.dialect.*;
import com.goldencode.p2j.persist.orm.*;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.ErrorManager;
import com.goldencode.p2j.util.logging.CentralLogger;
/**
* A collection useful static methods used to support the persistence framework.
*/
public final class DBUtils
{
/** The suffix added to the name of DMO interface to form the implementation class name .*/
public static final String IMPL_SUFFIX = "__Impl__";
/**
* The default maximum permitted size for all combined fields of an index. By default this
* is equal to Progress version 10.x limit of 1971 bytes (1972 is already in the red zone).
*/
public static final int DEFAULT_MAX_INDEX_SIZE = 1971;
/** The error message when a jdbc connection could not be established in
* {@link JdbcDataSource#getConnection(Database, DatabaseConfig, String) JdbcDataSource.getConnection()}.
*/
public static final String JDBC_CONNECTION_ERROR = "Error getting JDBC connection";
/** The c3p0 closed or broken resource pool error message.*/
public static final String BROKEN_POOL_ERROR = "Attempted to use a closed or broken resource pool";
/**
* The size of the id primary synthetic field (DatabaseManager.PRIMARY_KEY) added to tables,
* used by non-unique indexes. This is used for computing the index-key. Because it's of
* BIGINT / INT64 type so the it will always take 8 bytes.
*/
public static final int TABLE_ID_FIELD_SIZE = 8;
/** Logger */
private static final CentralLogger LOG = CentralLogger.get(DBUtils.class.getName());
/** Sets of non-nullable DMO properties, mapped by DMO implementation class */
private static final Map<Class<?>, Set<String>> notNullPropSets = new ConcurrentHashMap<>();
/**
* Convenience method to create an array of Hibernate <code>Type</code>
* objects to match the given array of substitution parameters. It is
* assumed that neither <code>args</code> nor any of its elements are
* <code>null</code>.
*
* @param args
* Array of HQL query substitution parameters.
*
* @return Array of matching, Hibernate <code>Type</code> instances.
*
* @throws IllegalArgumentException
* if <code>args</code> contains a parameter of unknown type.
*/
public static FqlType[] makeTypeArray(Object[] args)
{
return makeTypeArray(args, new FqlType[args.length]);
}
/**
* Convenience method to populate an array of {@code HQLTypes} objects to match the given array
* of substitution parameters. It is assumed that neither {@code args} nor any of its elements
* are {@code null}.
* <p>
* This implementation handles both resolved ({@code BaseDataType}) and unresolved
* ({@code Resolvable}) arguments. The latter is possible because we only care about the data
* type, not the actual value, of the parameters.
* <p>
* If any element in the {@code types} array is non-{@code null}, it is skipped.
*
* @param args
* Array of HQL query substitution parameters.
* @param types
* Array of the same size as {@code args}; some elements may be pre-populated.
*
* @return Array of matching, Hibernate {@code Type} instances.
*
* @throws IllegalArgumentException
* if {@code args} contains a parameter of unknown type.
*/
public static FqlType[] makeTypeArray(Object[] args, FqlType[] types)
{
int len = args.length;
for (int i = 0; i < len; i++)
{
if (types[i] != null)
{
// Type already known; don't replace it.
continue;
}
Object next = args[i];
Class<?> cls = (next instanceof Resolvable ? ((Resolvable) next).getType()
: next instanceof P2JQuery.ParamResolver ? character.class : next.getClass());
// look up the Hibernate type associated with the Java type:
types[i] = DataTypeHelper.getTypeClass(cls);
// TODO: don't think this is needed anymore; do we still use MANY-TO-ONE?
/*
// If no type matched, assume we are using an entity parameter.
if (type == null)
{
// This will throw IllegalArgumentException if class cannot be
// found among mapped, persistent classes.
Class<?> normalClass = DatabaseManager.normalizeDMOClass(cls);
String schema = DatabaseManager.getSchemaByClass(normalClass);
Configuration cfg = DatabaseManager.getConfigBySchema(schema);
type = cfg.getTypeResolver().getTypeFactory().manyToOne(normalClass.getName());
}
*/
}
return types;
}
/**
* Report the data type of a DMO property. Reflection is used to look up the field which backs
* the specified property. This will not work for list/composite properties, since these are
* not backed by a simple field. In these cases, the property name matches the CC prefixes
* the data type or error obtained from recursive function call for the 'pure' property name is
* reported.
*
* @param dmoIface
* DMO interface.
* @param property
* Unqualified DMO property name.
*
* @return Class which indicates the data type of the specified property.
*
* @throws PersistenceException
* if the property doesn't exist, or if there is some error determining its data type.
*/
public static Class<?> getDMOPropertyType(Class<? extends DataModelObject> dmoIface,
String property)
throws PersistenceException
{
if (DatabaseManager.PRIMARY_KEY.equals(property))
{
return Long.class;
}
if (property.startsWith("_"))
{
for (Method m : TempRecord.class.getMethods())
{
if (m.getReturnType() != void.class &&
m.getReturnType() != Void.class &&
m.getName().equals(property))
{
return m.getReturnType();
}
}
}
Map<String, Method> legacyGetters = PropertyHelper.allGettersByProperty(dmoIface);
Method getter = legacyGetters.get(property);
if (getter == null)
{
// second chance: the property name is decorated because it is a computed column
// TODO: support dialect-specific prefix ?
String pureProperty = null;
if (property.startsWith(DialectHelper.SENSITIVE_CHAR_FIELD))
{
pureProperty = property.substring(DialectHelper.SENSITIVE_CHAR_FIELD.length());
}
else if (property.startsWith(DialectHelper.INSENSITIVE_CHAR_FIELD))
{
pureProperty = property.substring(DialectHelper.INSENSITIVE_CHAR_FIELD.length());
}
if (pureProperty != null)
{
return getDMOPropertyType(dmoIface, pureProperty);
}
final String msg = "Property %s for DMO type %s is not recognized";
throw new PersistenceException(String.format(msg, property, dmoIface.getName()));
}
return getter.getReturnType();
}
/**
* Compose a DMO property name within the given string buffer. For
* non-text properties, this will simply emit the property name, qualified
* by the proper DMO alias. For text properties, however, the qualified
* property name is enclosed within a SQL function which trims trailing
* whitespace. For case-insensitive text properties, this expression is
* enclosed by a SQL function which uppercases the trimmed result string.
* For a text property whose qualified name is <code>foo.bar</code>, the
* end result would be:
* <pre>
* upper(rtrim(foo.bar))
* </pre>
* For a non-text property <code>foo.bar</code>, <code>foo.bar</code> is emitted.
*
* @param isCharacter
* <code>true</code> for text properties; <code>false</code> for
* non-text properties.
* @param ignoreCase
* <code>true</code> if the name expression must be wrapped in an
* uppercasing function; <code>false</code> for case-sensitive
* text properties, as well as for non-text properties.
* @param computedColumnPrefix
* If a computed column is needed instead of embedding the DMO
* property name within a function, this parameter should be the
* non-null prefix to be prepended before the property name.
* If <code>null</code>, it is assumed function use is acceptable.
* @param alias
* Optional alias used to qualify <code>name</code>. Ignored if <code>null</code>.
* @param name
* Unqualified DMO property name.
* @param buf
* String buffer into which results are emitted.
* @param dialect
* The dialect used for <code>rtrim</code> sql function. If <code>isCharacter</code>
* is <code>false</code> this parameter is ignored.
*/
public static void composePropertyName(boolean isCharacter,
boolean ignoreCase,
String computedColumnPrefix,
String alias,
String name,
StringBuilder buf,
Dialect dialect)
{
String qualifier = "";
if (alias != null)
{
qualifier = alias + '.';
}
if (!isCharacter)
{
buf.append(qualifier).append(name);
return;
}
if (computedColumnPrefix != null)
{
buf.append(qualifier);
buf.append(getPrefixedParameter(name, computedColumnPrefix));
return;
}
Consumer<String> checkRtrim = (String qual) ->
{
if (!dialect.isAutoRtrim())
{
// use dialect-specific rtrim function
dialect.addRtrimmedExpression(qual + name, buf);
}
else
{
buf.append(qual).append(name);
}
};
if (!dialect.isAutoCi())
{
if (ignoreCase)
{
buf.append("upper(");
}
checkRtrim.accept(qualifier);
if (ignoreCase)
{
buf.append(")");
}
}
else
{
checkRtrim.accept(qualifier);
}
}
/**
* Generates an unique alias for a table to be used in subqueries. This is used in order to
* Hibernate generate correct SQL queries in some particular cases when nested selects are
* used.
* <p>
* For the moment the implementation an usage may be seen as too generic, it will cover all
* subselect cases, not only the particular case where Hibernate leaks the {@code table.id} of
* the table from outer select in place of {@code table.table} field of the inner select.
* If needed, future revisions of the method might need additional parameters to support
* multiple nesting select.
*
* @param tableName
* The base name of the table.
*
* @return The unique alias that will be valid only in the scope of the inner select.
*/
public static String getSubselectAlias(String tableName)
{
return tableName + "_1";
}
/**
* Get the DMO implementation class for the given DMO entity name.
*
* @param entityName
* DMO entity name.
*
* @return DMO implementation class.
*
* @throws IllegalArgumentException
* if <code>entityName</code> does not represent a loaded class,
* or if there is any other error loading the class.
*
* @deprecated use {@link DmoMetadataManager#getDmoInfo(String, String)} for faster result with a richer
* feature result structure.
*/
@SuppressWarnings("unchecked")
@Deprecated
public static Class<? extends Record> dmoClassForEntity(String entityName)
{
try
{
return (Class<? extends Record>) Class.forName(entityName,
true,
MultiClassLoader.getClassLoader());
}
catch (Exception exc)
{
throw new IllegalArgumentException("Invalid DMO entity name: " + entityName, exc);
}
}
/**
* Generate the DMO implementation class name (the entity name), given a
* DMO interface. This codifies the convention of adding the text "__Impl__"
* to the end of the interface name to come up with the implementation
* name.
*
* @param dmoIface
* DMO interface.
*
* @return DMO implementation class name.
*
* @deprecated unused(?)
*/
@Deprecated
public static String entityForDMOInterface(Class<? extends DataModelObject> dmoIface)
{
return dmoIface.getName() + IMPL_SUFFIX;
}
/**
* Extracts index name from index name optionally containing prefix "idx__" and backticks.
*
* @param indexName
* Index name.
*
* @return Extracts index name from index name optionally containing prefix "idx__" and
* backticks.
*/
public static String extractIndexName(String indexName)
{
StringBuilder buf = new StringBuilder();
boolean escaped = indexName.startsWith("`");
if (escaped)
{
indexName = indexName.substring(1, indexName.length() - 1);
}
if (indexName.startsWith("idx__"))
{
// eliminate the prefix
indexName = indexName.substring("idx__".length());
}
buf.append(indexName);
if (escaped)
{
buf.insert('`', 0);
buf.append('`');
}
return buf.toString();
}
/**
* Returns a formatted string using the specified format string and parameter optionally
* containing backticks.
*
* @param format
* Format string.
* @param parameter
* String parameter optionally containing backticks..
*
* @return Returns a formatted string using the specified format string and parameter
* optionally containing backticks.
*/
public static String formatWithEscapedParameter(String format, String parameter)
{
return formatWithEscapedParameter(format, parameter, null);
}
/**
* Returns a formatted string using the specified format string and arguments optionally
* containing backticks.
*
* @param format
* Format string.
* @param parameter
* String parameter optionally containing backticks..
* @param index
* Nullable value.
*
* @return Returns a formatted string using the specified format string and arguments
* optionally containing backticks.
*/
public static String formatWithEscapedParameter(String format, String parameter, Long index)
{
if (parameter.startsWith("`"))
{
parameter = parameter.substring(1, parameter.length() - 1);
format = '`' + format + '`';
}
if (index == null)
{
return String.format(format, parameter);
}
else
{
return String.format(format, parameter, index);
}
}
/**
* Returns prefixed parameter optionally containing backticks.
*
* @param parameter
* Parameter optionally containing backticks.
* @param prefix
* Prefix.
*
* @return Returns prefixed parameter optionally containing backticks.
*/
public static String getPrefixedParameter(String parameter, String prefix)
{
StringBuilder buf = new StringBuilder();
if (parameter.startsWith("`"))
{
parameter = parameter.substring(1);
buf.append('`');
}
buf.append(prefix);
buf.append(parameter);
return buf.toString();
}
/**
* Returns a formatted string using the specified format string and arguments optionally
* containing backticks.
*
* @param format
* Format string.
* @param table
* String parameter optionally containing backticks.
* @param params
* String parameters optionally containing backticks.
*
* @return Returns a formatted string using the specified format string and arguments
* optionally containing backticks.
*/
public static String formatWithEscapedParameters(String format, String table, String... params)
{
Object[] formatParams = new Object[params.length + 1];
if (table.startsWith("`"))
{
format = '`' + format + '`';
formatParams[0] = table.substring(1, table.length() - 1);
for (int i = 0; i < params.length; i++) {
if (params[i].startsWith("`"))
{
formatParams[i + 1] = params[i].substring(1, params[i].length() - 1);
}
else
{
formatParams[i + 1] = params[i];
}
}
}
else
{
formatParams[0] = table;
System.arraycopy(params, 0, formatParams, 1, params.length);
}
return String.format(format, formatParams);
}
/**
* Handle an <code>InterruptedException</code>, which may be wrapped in an
* exception causal chain, by throwing <code>StopConditionException</code>.
* Alternatively, if the error represents a database connection problem,
* <code>DatabaseConnectionException</code> is thrown. Determination of
* the latter type of problem is dialect-specific.
* <p>
* If neither of these conditions is found to be the cause of the error,
* this method returns normally.
*
* @param database
* The database in use when the exception was thrown. May be
* <code>null</code> if no database is used.
* @param exc
* An exception to inspect.
*
* @throws StopConditionException
* if <code>exc</code> or any exception in its causal chain is an
* instance of <code>InterruptException</code>.
* @throws DatabaseConnectionException
* if <code>exc</code> or any exception in its causal chain is
* determined to be a database connection error.
*/
public static void handleException(Database database, Exception exc)
{
if (database != null)
{
Dialect dialect = DatabaseManager.getDialect(database);
if (dialect == null ||
dialect.isConnectionError(exc) ||
exc.getMessage().equals(JDBC_CONNECTION_ERROR))
{
ErrorManager.displayAbend(1006, "Database " + database.getName() + " not connected");
ErrorManager.displayError(492, "Unable to run startup procedure");
LOG.log(Level.SEVERE, exc.getMessage(), exc);
throw new DatabaseConnectionException(exc, database);
}
}
Throwable next = exc;
while (next != null)
{
if (next instanceof InterruptedException)
{
ErrorManager.handleStopException(new StopConditionException(exc));
}
next = next.getCause();
}
}
/**
* Get field reference(s) for ADD-LIKE-COLUMN. Includes error handling.
*
* @param query
* The query which backs the browse ADD-LIKE-COLUMN was called for.
* @param fieldHandle
* Handle to the target field.
*
* @return reference to the target field if <code>fieldHandle</code> is valid and
* references a buffer which participates in the query, <code>null</code> otherwise.
* If <code>fieldHandle</code> references an extent field, multiple field references
* for each extent element are returned.
*/
public static FieldReference[] getFieldForAddLikeColumn(QueryWrapper query, handle fieldHandle)
{
if (!fieldHandle._isValid())
{
return null;
}
WrappedResource resource = fieldHandle.getResource();
if (!BufferField.class.isAssignableFrom(resource.getClass()))
{
ErrorManager.recordOrShowError(9162,
"ADD-LIKE-COLUMN requires valid buffer-field object",
false, false);
return null;
}
BufferFieldImpl srcBufferField = (BufferFieldImpl) resource;
BufferImpl srcBuffer = (BufferImpl) srcBufferField.getBuffer();
boolean match = false;
int bufferCount = query.numBuffers().intValue();
for (int i = 1; i <= bufferCount; i++)
{
Buffer buffer = query.getBufferByIndex(i);
if (buffer.equals(srcBuffer))
{
match = true;
break;
}
}
if (!match)
{
ErrorManager.recordOrShowError(9163,
"In ADD-LIKE-COLUMN, Cannot find field in browser's query",
false, false);
return null;
}
String legacyFieldName = srcBufferField._name();
RecordBuffer rbuff = srcBuffer.buffer();
Property property = rbuff.getDmoInfo().byLegacyName(legacyFieldName);
int extent = srcBufferField._extent();
if (extent > 1)
{
FieldReference[] res = new FieldReference[extent];
for (int i = 0; i < extent; i++)
{
res[i] = new FieldReference((DataModelObject) srcBuffer, property.name, i);
}
return res;
}
else
{
return new FieldReference[] { new FieldReference((DataModelObject) srcBuffer, property.name) };
}
}
/**
* Get field reference(s) for ADD-LIKE-COLUMN. Includes error handling.
*
* @param query
* The query which backs the browse ADD-LIKE-COLUMN was called for.
* @param fieldExpression
* Expression which specifies the target field. Has format <code>[database
* name.]{legacy table name|legacy buffer name}.legacy field name</code>.
*
* @return reference to the target field if the field expression is valid and references a
* field of a buffer which participates in the query, <code>null</code> otherwise.
* If the field expression references an extent field, multiple field references
* for each extent element are returned.
*/
public static FieldReference[] getFieldForAddLikeColumn(QueryWrapper query, character fieldExpression)
{
String fullSource = fieldExpression.getValue();
if (fullSource == null)
{
return null;
}
int pos = fullSource.lastIndexOf('.');
if (pos < 0)
{
ErrorManager.recordOrShowError(9158,
String.format("%s in ADD-LIKE-COLUMN must have a table qualifier", fullSource),
false, false);
return null;
}
String srcFieldName = fullSource.substring(pos + 1);
String fullTableName = fullSource.substring(0, pos);
String[] tblName = DynamicTablesHelper.parseTableName(fullTableName);
String srcBufferName = tblName[0];
String srcDatabaseName = tblName[1];
// Buffer selection priorities.
// 1 - Same buffer name, same database.
// 2 - Default buffer name (i.e. table name), same database.
// 3 - Same buffer name, no database specified.
// 4 - Default buffer name, no database specified.
// 5 - No match.
// TODO works differently in GUI!
Buffer selectedBuffer = null;
int selectedPriority = 5;
int bufferCount = query.numBuffers().intValue();
for (int i = 1; i <= bufferCount; i++)
{
BufferImpl buffer = (BufferImpl) query.getBufferByIndex(i);
RecordBuffer recordBuffer = ((BufferImpl) buffer).buffer();
String bufferName = buffer._name();
String defaultBufferName = TableMapper.getLegacyName(recordBuffer);
String databaseName = recordBuffer.getDatabase().getName();
boolean databaseMatch = srcDatabaseName != null && srcDatabaseName.equals(databaseName);
boolean nameMatch = srcBufferName.equals(bufferName);
boolean defaultNameMatch = srcBufferName.equals(defaultBufferName);
int priority = 5;
if (databaseMatch && nameMatch)
priority = 1;
else if (databaseMatch && defaultNameMatch)
priority = 2;
else if (nameMatch)
priority = 3;
else if (defaultNameMatch)
priority = 4;
if (priority < selectedPriority)
{
selectedBuffer = buffer;
selectedPriority = priority;
}
}
if (selectedBuffer == null)
{
ErrorManager.recordOrShowError(9159,
String.format("Table %s specified in ADD-LIKE-COLUMN not found in browser's QUERY",
fullTableName),
false, false);
return null;
}
int extentIndex = -1;
if (srcFieldName.matches("^.*\\[.*]$"))
{
pos = srcFieldName.lastIndexOf('[');
String indexStr = srcFieldName.substring(pos + 1, srcFieldName.length() - 1);
try
{
extentIndex = Integer.parseInt(indexStr);
}
catch (NumberFormatException e)
{
// suppress
}
if (extentIndex == -1)
{
ErrorManager.recordOrShowError(9160,
String.format("In ADD-LIKE-COLUMN, invalid array index specified for %s",
srcFieldName),
false, false);
return null;
}
srcFieldName = srcFieldName.substring(0, pos);
}
RecordBuffer rbuff = RecordBuffer.get((DataModelObject) selectedBuffer);
DmoMeta dmoInfo = rbuff.getDmoInfo();
Property property = dmoInfo.byLegacyName(srcFieldName);
if (property == null)
{
ErrorManager.recordOrShowError(9161,
"In ADD-LIKE-COLUMN, could not find source field " + srcFieldName,
false, false);
return null;
}
String propertyName = property.name;
int extentSize = property.extent;
FieldReference[] res;
if (extentIndex != -1)
{
if (extentIndex < 1 || extentIndex > extentSize)
{
ErrorManager.recordOrShowError(
7365,
String.format("%s BUFFER-VALUE argument must be valid array index, less than " +
"or equal to field extent %d", srcFieldName, extentSize),
false, false);
return null;
}
res = new FieldReference[] {new FieldReference((DataModelObject) selectedBuffer,
propertyName,
extentIndex - 1)};
}
else if (extentSize > 1)
{
res = new FieldReference[extentSize];
for (int i = 0; i < extentSize; i++)
res[i] = new FieldReference((DataModelObject) selectedBuffer, propertyName, i);
}
else
{
res = new FieldReference[] {new FieldReference((DataModelObject) selectedBuffer,
propertyName)};
}
return res;
}
/**
* Find the field of a buffer participating in the given query, by the field name.
*
* @param query
* Query which manages the buffer which contains the target field.
* @param fullFieldName
* 4GL name of the target field in format <code>field-name</code> or
* <code>buffer-name.field-name</code>.
*
* @return reference to the specified field.
*
* @throws NumberedException
* <p>Error with code <code>1</code>: field with the given name cannot be found.</p>
* <p>Error with code <code>2</code>: if the buffer name is not specified, there are multiple
* buffers containing the field with the give name.</p>
*/
public static BufferField getMatchingBufferField(QueryWrapper query, String fullFieldName)
throws NumberedException
{
BufferImpl targetBuffer = null;
String fieldName;
String bufName = null;
fullFieldName = fullFieldName.trim();
int pos = fullFieldName.lastIndexOf('.');
if (pos < 0)
{
fieldName = fullFieldName;
}
else
{
fieldName = fullFieldName.substring(pos + 1);
bufName = fullFieldName.substring(0, pos);
}
int numBuffers = query.numBuffers().intValue();
for (int i = 1; i <= numBuffers; i++)
{
BufferImpl buffer = (BufferImpl) query.getBufferByIndex(i);
if (bufName != null && !bufName.equalsIgnoreCase(buffer._name()))
{
// mismatching buffer name
continue;
}
String property = TableMapper.getPropertyName(buffer.buffer(), fieldName);
if (property == null)
{
// mismatching field name
continue;
}
if (bufName != null)
{
// matching field name and buffer name
return buffer.bufferFieldNative(fieldName);
}
else
{
if (targetBuffer != null)
{
// There are multiple buffers with the same field name
throw new NumberedException("", 2);
}
else
{
targetBuffer = buffer;
}
}
}
if (targetBuffer == null)
{
// There's no buffer with the given field
throw new NumberedException("", 1);
}
return targetBuffer.bufferFieldNative(fieldName);
}
/**
* This method returns return type or parameter type depending on getter/setter method name
* prefix.
*
* @param method
* Method.
*
* @return Return type or parameter type depending on getter/setter method name prefix.
*/
static Class<?> getGetterSetterType(Method method)
{
String methodName = method.getName();
if (methodName.startsWith("set"))
{
Class<?>[] parameterTypes = method.getParameterTypes();
// if indexed: first parameter is array index, second one is value
return parameterTypes[parameterTypes.length - 1];
}
else if (methodName.startsWith("get") || methodName.startsWith("is"))
{
return method.getReturnType();
}
else if (method.equals(BaseRecord.PK_GETTER))
{
return rowid.class;
}
else
{
throw new UnsupportedOperationException(
"Method getGetterSetterType is acceptable only for getters/setters");
}
}
/**
* Look up the getter method associated with the given property. Tests
* for both <code>get</code> and <code>is</code> variants.
*
* @param dmoClass
* DMO implementation class.
* @param propName
* Unqualified DMO property name.
* @param parmTypes
* Array of data types which are expected by the getter method.
* This will generally be <code>null</code> (or an empty array),
* or an array with one element of type <code>Integer.TYPE</code>
* (representing the index value passed to an indexed getter
* method).
*
* @return Getter method for the given property.
*
* @throws PersistenceException
* if <code>propName</code> does not represent a property of
* <code>dmoClass</code>.
*/
static Method findDMOGetterMethod(Class<? extends Record> dmoClass,
String propName,
Class<?>[] parmTypes)
throws PersistenceException
{
String methName;
try
{
// First try with a "get" prefix (the majority of cases).
methName = StringHelper.generateBeanName(propName, true, false);
return dmoClass.getDeclaredMethod(methName, parmTypes);
}
catch (NoSuchMethodException exc)
{
}
try
{
// As a fallback, try with an "is" prefix, in case property is a logical.
methName = StringHelper.generateBeanName(propName, true, true);
return dmoClass.getDeclaredMethod(methName, parmTypes);
}
catch (NoSuchMethodException exc2)
{
throw new PersistenceException(
dmoClass.getName() + " does not contain property '" + propName + "'");
}
}
/**
* Compose a DMO property name within the given string buffer. For
* non-text properties, this will simply emit the property name, qualified
* by the proper DMO alias. For text properties, however, the qualified
* property name is enclosed within a SQL function which trims trailing
* whitespace. For case-insensitive text properties, this expression is
* enclosed by a SQL function which uppercases the trimmed result string.
* For a text property whose qualified name is <code>foo.bar</code>, the
* end result would be:
* <pre>
* upper(rtrim(foo.bar))
* </pre>
* <p>
* For a non-text property <code>foo.bar</code>, <code>foo.bar</code> is
* emitted.
*
* @param buffer
* Record buffer which holds information about the DMO's alias,
* interface, and implementation class.
* @param name
* Unqualified DMO property name.
* @param buf
* String buffer into which results are emitted.
*
* @throws PersistenceException
* if any errors occur looking up metadata regarding the property.
*/
static void composePropertyName(RecordBuffer buffer, String name, StringBuilder buf)
throws PersistenceException
{
composePropertyName(buffer, name, buf, Database.Type.PRIMARY);
}
/**
* Compose a DMO property name within the given string buffer. For
* non-text properties, this will simply emit the property name, qualified
* by the proper DMO alias. For text properties, however, the qualified
* property name may be enclosed within a SQL function which trims trailing
* whitespace. For case-insensitive text properties, this expression is
* enclosed by a SQL function which uppercases the trimmed result string.
* For a text property whose qualified name is <code>foo.bar</code>, the
* end result would be:
* <pre>
* upper(rtrim(foo.bar))
* </pre>
* <p>
* In cases where a special computed column name is required instead of the
* enclosing function syntax noted above, such a name is generated instead.
* <p>
* For a non-text property <code>foo.bar</code>, <code>foo.bar</code> is
* emitted.
*
* @param buffer
* Record buffer which holds information about the DMO's alias,
* interface, and implementation class.
* @param name
* Unqualified DMO property name.
* @param buf
* String buffer into which results are emitted.
* @param type
* Database type, which drives the dialect to use to compose the name.
*/
static void composePropertyName(RecordBuffer buffer, String name, StringBuilder buf, Database.Type type)
{
if (name == null)
{
buf.append(Session.PK);
return;
}
boolean isChar = false;
boolean ignoreCase = false;
String computedColumnPrefix = null;
Dialect dialect = null;
DmoMeta dmoInfo = buffer.getDmoInfo();
Property propInfo = dmoInfo.getFieldInfo(name);
if (propInfo._isCharacter)
{
isChar = true;
ignoreCase = !propInfo.caseSensitive;
}
if (isChar)
{
if (!Database.Type.PRIMARY.equals(type))
{
Database secDB = buffer.getDatabase().toType(type);
dialect = DatabaseManager.getDialect(secDB);
}
if (dialect == null)
{
dialect = buffer.getDialect();
}
if (dialect.needsComputedColumns())
{
Boolean ccIgnoreCase = dmoInfo.isIndexedIgnoreCase(name);
if (ccIgnoreCase != null)
{
computedColumnPrefix = dialect.getComputedColumnPrefix(!ignoreCase);
}
}
}
composePropertyName(isChar, ignoreCase, computedColumnPrefix, buffer.getDMOAlias(), name, buf, dialect);
}
/**
* Right-trim array according to the query's table count.
*
* @param array
* Array to trim.
* @param query
* Target query.
*
* @return array right-trimed according to the query's table count.
*/
static Long[] trimToTableCount(Long[] array, P2JQuery query)
{
int tableCount = query.getTableCount();
if (array != null && array.length > tableCount)
{
Long[] newArray = new Long[tableCount];
System.arraycopy(array, 0, newArray, 0, tableCount);
array = newArray;
}
return array;
}
}