# 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;
       }
    }