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