# PropertyHelper.java

    /*
    ** Module   : PropertyHelper.java
    ** Abstract : Contains helper methods related to DMO properties
    **
    ** Copyright (c) 2004-2024, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
    ** 001 ECF 20051113   @23390 Created initial version. Contains helper
    **                           methods related to DMO properties. Uses
    **                           introspection to create maps of different
    **                           property access categories: getters, setters,
    **                           and validators.
    ** 002 ECF 20060504   @26000 Added sizersByProperty method. Ensure correct
    **                           method variant is selected if methods are
    **                           overloaded.
    ** 003 ECF 20060908   @29381 Fixed defect in makePropertyName(). The
    **                           algorithm was unconditionally decapitalizing
    **                           the property name's first letter, even when
    **                           it should have remained uppercase.
    ** 004 ECF 20070531   @33908 Added methods to map methods to property
    **                           names. Added caching of property/method maps
    **                           to avoid redundant introspection.
    ** 005 ECF 20070531   @33913 Removed validatorsByProperty(). This method
    **                           is no longer used and was performing
    **                           unnecessary introspection.
    ** 006 ECF 20080521   @38500 Added settersByProperty(String) method
    **                           variant. Integrated generics.
    ** 007 ECF 20080605   @38609 Made settersByProperty(String) method more
    **                           lenient. It was throwing an
    **                           IllegalStateException if invoked before
    **                           settersByProperty(Class<?>).
    ** 008 ECF 20080925   @39947 Changed makePropertyName(). Property name is
    **                           now intern()'d to save memory.
    ** 009 ECF 20090217   @41331 Added gettersForInterface() and backing map.
    **                           Provides set of getter methods for a given
    **                           DMO interface.
    ** 010 ECF 20090315   @41606 Added lazyExtentTriggersForInterface() and
    **                           backing map. Provides list of getter methods
    **                           which will trigger lazy initialization of the
    **                           persistent collections used to manage extent
    **                           fields in a DMO.
    ** 011 ECF 20090815   @43633 Added propertiesByGetter(), extentsByProperty().
    **                           Needed by RecordBuffer.
    ** 012 ECF 20090817   @43681 Fixed gettersForInterface(). This method was not
    **                           picking up the new getters for extent fields
    **                           which accept NumberType for the index parameter.
    ** 013 VMN 20140328          Added method getPropertyName() and support for custom
    **                           denormalization of fields with extent.
    ** 014 HC  20140613          Javadocs added.
    ** 015 VMN 20150317          Added method isJavaBeanMethod().
    ** 016 ECF 20150801          Code cleanup. Throw IllegalArgumentException instead of
    **                           UnsupportedOperationException from getPropertyName. Improve
    **                           performance through more judicious use of String.intern.
    ** 017 OM  20150826          Added getDmoDeclaredMethods() for method lookup in super interface.
    ** 018 EVL 20160223          Javadoc fixes to make compatible with Oracle Java 8 for Solaris 10.
    ** 019 ECF 20160222          Complete rewrite. The new implementation supports both normalized
    **                           and denormalized extent fields and should be more efficient when
    **                           accessed by multiple threads.
    ** 020 ECF 20171024          Widened access to certain APIs to enable use from other packages.
    ** 021 ECF 20171231          Performance improvement.
    ** 022 OM  20180614          Added support for bulk-accessors of extent fields.
    ** 023 ECF 20190306          Modified lazyExtentTriggersForInterface due to TableMapper change.
    ** 024 CA  20190812          Added isLoaded().  See the comments in the method for the limitations.
    ** 025 ECF 20200906          New ORM implementation.
    ** 026 OM  20200919          Improved access to dmo metadata.
    **     OM  20210309          Do not use DmoMeta as key in TableMapper because temp-tables share the same
    **                           DmoMeta instance.
    **     OM  20220707          Exposed legacyGettersByProperty() and legacySettersByProperty as public.
    ** 027 CA  20240331          Use logical constants for TRUE, FALSE, UNKNOWN, within internal FWD runtime, so
    **                           logical type checks must include sub-classes, too.
    */

    /*
    ** 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.beans.Introspector;
    import java.lang.reflect.*;
    import java.util.*;
    import java.util.concurrent.*;
    import java.util.function.*;
    import com.goldencode.p2j.persist.orm.*;
    import com.goldencode.p2j.util.*;

    /**
     * Helper class which uses introspection to manage maps of different categories of DMO property
     * related methods and metadata:  getters, setters, extent sizes, and lazy collection
     * initializers.
     * <p>
     * Maps are threadsafe and are cached to avoid redundant introspection using reflection.
     */
    public final class PropertyHelper
    {
       /** Method name prefix(es) for getter methods */
       private static final String[] getterPrefix = new String[] { "get", "is"};

       /** Method name prefix(es) for setter methods */
       private static final String[] setterPrefix = new String[] { "set" };

       /** Special method name for primary key (used for both getter and setter (arguments vary) */
       private static final String primaryKey = "primaryKey";

       /** Cache of property name to getter method maps, by DMO interface, for legacy fields only */
       private static final Map<Class<?>, Map<String, Method>> p2lgCache = new ConcurrentHashMap<>();

       /** Cache of property name to setter method maps, by DMO interface, for legacy fields only */
       private static final Map<Class<?>, Map<String, Method>> p2lsCache = new ConcurrentHashMap<>();

       /**
        * Cache of property name to bulk getter method maps, by DMO interface, for legacy fields only
        */
       private static final Map<Class<?>, Map<String, Method>> bp2lgCache = new ConcurrentHashMap<>();

       /**
        * Cache of property name to bulk setter method maps, by DMO interface, for legacy fields only
        */
       private static final Map<Class<?>, Map<String, Method>> bp2lsCache = new ConcurrentHashMap<>();

       /** Cache of property name to getter method maps, by DMO interface, for POJO methods only */
       private static final Map<Class<?>, Map<String, Method>> p2pgCache = new ConcurrentHashMap<>();

       /** Cache of property name to setter method maps, by DMO interface, for POJO methods only */
       private static final Map<Class<?>, Map<String, Method>> p2psCache = new ConcurrentHashMap<>();

       /** Cache of property name to getter method maps, by DMO interface, POJO and legacy methods */
       private static final Map<Class<?>, Map<String, Method>> p2agCache = new ConcurrentHashMap<>();

       /** Cache of property name to setter method maps, by DMO interface, POJO and legacy methods */
       private static final Map<Class<?>, Map<String, Method>> p2asCache = new ConcurrentHashMap<>();

       /** Cache of property name to extent maps, by DMO interface */
       private static final Map<Class<?>, Map<String, Integer>> p2xCache = new ConcurrentHashMap<>();

       /** Cache of getter method to property name maps, by DMO interface, for legacy fields only */
       private static final Map<Class<?>, Map<Method, String>> g2lCache = new ConcurrentHashMap<>();

       /** Cache of setter method to property name maps, by DMO interface, for legacy fields only */
       private static final Map<Class<?>, Map<Method, String>> s2lCache = new ConcurrentHashMap<>();

       /** Cache of getter method to property name maps, by DMO interface, for POJO methods only */
       private static final Map<Class<?>, Map<Method, String>> g2pCache = new ConcurrentHashMap<>();

       /** Cache of setter method to property name maps, by DMO interface, for POJO methods only */
       private static final Map<Class<?>, Map<Method, String>> s2pCache = new ConcurrentHashMap<>();

       /** Cache of property name to setter method maps, by DMO entity name, for POJO methods only */
       private static final Map<String, Map<String, Method>> p2seCache = new ConcurrentHashMap<>();

       /** Cache of methods to trigger lazy collection init, by DMO interface */
       private static final Map<Class<?>, List<Method>> i2ltCache = new ConcurrentHashMap<>();

       static
       {
          // special case: the Persistable interface must be represented in the all-getters map,
          // but it will never be found in a legacy-getters map, so we add it explicitly here
          try
          {
             Method getter = Persistable.class.getDeclaredMethod("primaryKey");
             getter.setAccessible(true);
             Map<String, Method> map = Collections.singletonMap(Session.PK, getter);
             p2agCache.put(Persistable.class, map);
          }
          catch (NoSuchMethodException exc)
          {
             throw new RuntimeException(exc);
          }
       }

       /**
        * Private constructor;  all access is via static methods.
        */
       private PropertyHelper()
       {
       }

       /**
        * Get a map of DMO property names to POJO getter methods for the given DMO type. POJO methods
        * are those that are declared only by the top-level DMO interface, excluding those declared
        * by the enclosed, DMO buffer sub-interface.
        *
        * @param   dmoIface
        *          DMO interface (top level) for which the map is desired.
        *
        * @return  A map as described above.
        */
       public static Map<String, Method> pojoGettersByProperty(Class<? extends DataModelObject> dmoIface)
       {
          return pojoMethodsByProperty(dmoIface, p2pgCache, getterPrefix);
       }

       /**
        * Get a map of DMO property names to POJO setter methods for the given DMO type. POJO methods
        * are those that are declared only by the top-level DMO interface, excluding those declared
        * by the enclosed, DMO buffer sub-interface.
        *
        * @param   dmoIface
        *          DMO interface (top level) for which the map is desired.
        *
        * @return  A map as described above.
        */
       public static Map<String, Method> pojoSettersByProperty(Class<? extends DataModelObject> dmoIface)
       {
          return pojoMethodsByProperty(dmoIface, p2psCache, setterPrefix);
       }

       /**
        * Get a map of DMO property names to all legacy and POJO getter methods for the given DMO
        * type.
        *
        * @param   dmoIface
        *          DMO interface (top level) for which the map is desired.
        *
        * @return  A map as described above.
        *
        * @see     #legacyGettersByProperty(Class)
        * @see     #pojoGettersByProperty(Class)
        */
       public static Map<String, Method> allGettersByProperty(Class<?> dmoIface)
       {
          return allMethodsByProperty(dmoIface, p2agCache, p2lgCache, p2pgCache, getterPrefix);
       }

       /**
        * Get a map of DMO property names to all legacy and POJO setter methods for the given DMO
        * type.
        *
        * @param   dmoIface
        *          DMO interface (top level) for which the map is desired.
        *
        * @return  A map as described above.
        *
        * @see     #legacyGettersByProperty(Class)
        * @see     #pojoGettersByProperty(Class)
        */
       public static Map<String, Method> allSettersByProperty(Class<? extends DataModelObject> dmoIface)
       {
          return allMethodsByProperty(dmoIface, p2asCache, p2lsCache, p2psCache, setterPrefix);
       }

       /**
        * Given a DMO implementation class entity name, get the map of DMO property names to setter
        * methods.
        *
        * @param   entity
        *          DMO implementation class entity name.
        *
        * @return  Map of DMO properties to setter methods.
        */
       public static Map<String, Method> pojoSettersByProperty(String entity)
       {
          Map<String, Method> map = p2seCache.get(entity);

          if (map == null)
          {
             Class<? extends DataModelObject> dmoIface =
                   DmoMetadataManager.getDmoInfo(entity, null).getAnnotatedInterface();

             map = pojoSettersByProperty(dmoIface);
             Map<String, Method> existing = p2seCache.putIfAbsent(entity, map);
             if (existing != null)
             {
                map = existing;
             }
          }

          return map;
       }

       /**
        * Get the property name for the given getter or setter method. The property name is the
        * decapitalized root name of a bean-like method name which starts with one of the prefixes
        * <code>get</code>, <code>set</code>, or <code>is</code>. If the method name begins with none
        * of these prefixes, an error is raised.
        *
        * @param   method
        *          Method.
        *
        * @return  Property name.
        *
        * @throws  IllegalArgumentException
        *          if method is not getter or setter.
        */
       public static String getPropertyName(Method method)
       {
          String methodName = method.getName();
          int index;
          if (methodName.startsWith("get") || methodName.startsWith("set"))
          {
             index = 3;
          }
          else if (methodName.startsWith("is"))
          {
             index = 2;
          }
          else
          {
             throw new IllegalArgumentException(
                "Method is neither a getter nor a setter: " + method);
          }

          return makePropertyName(methodName, index);
       }

       /**
        * Get an immutable map of DMO property names to extent values (if any) for the given DMO type.
        * If a DMO has no converted extent fields, the map will be empty.
        * <p>
        * <strong>WARNING:</strong> a mapping for any given DMO type is only available after the
        * first time a record buffer is defined for that DMO type. The information is collected and
        * stored when a DMO interface is first proxied, so this method must not be invoked before
        * that occurs. To do so represents a programming error and will raise an exception.
        *
        * @param   dmoIface
        *          DMO interface.
        *
        * @return  A map as described above.
        *
        * @throws  IllegalStateException
        *          if this method is invoked before the given DMO interface has been proxied.
        */
       public static Map<String, Integer> extentsByProperty(Class<? extends DataModelObject> dmoIface)
       {
          return getMapFailOnNull(dmoIface, p2xCache);
       }

       /**
        * Check if the cache maps have been loaded.  False negatives may result, as only
        * {@link #bp2lsCache} is checked, but this is OK, as the false negatives will be until this
        * map gets populated.
        *
        * @param    dmoIface
        *           The DMO interface to check.
        *
        * @return   <code>true</code> if the DMO iface cache maps have been loaded.
        */
       static boolean isLoaded(Class<?> dmoIface)
       {
          // TODO: there is a race condition here, as the cache maps are not initialized in an atomic
          // way; and other threads may try to use them before all of them get initialized, even if
          // this reports true;
          // for now, here it uses bp2lsCache map to check, as this is the LAST of the cache to be set
          // by DmoProxyPlugin
          return bp2lsCache.containsKey(dmoIface);
       }

       /**
        * Cache an immutable map of DMO property names to extent values (if any) for the given DMO
        * type, keyed by DMO interface.
        * <p>
        * This method is only intended to be called once per DMO interface. The map for a given DMO
        * interface can only be stored once. Further requests to store a new map for that interface
        * will fail silently and the original mapping will remain.
        *
        * @param   dmoIface
        *          DMO interface.
        * @param   map
        *          A map as described above.
        */
       static void putExtentsByProperty(Class<? extends DataModelObject> dmoIface, Map<String, Integer> map)
       {
          p2xCache.putIfAbsent(dmoIface, storableMap(map));
       }

       /**
        * Get an immutable map of DMO property names to legacy getter methods for the given DMO type.
        * <p>
        * <strong>WARNING:</strong> a mapping for any given DMO type is only available after the
        * first time a record buffer is defined for that DMO type. The information is collected and
        * stored when a DMO interface is first proxied, so this method must not be invoked before
        * that occurs. To do so represents a programming error and will raise an exception.
        *
        * @param   dmoIface
        *          DMO interface.
        *
        * @return  A map as described above.
        *
        * @throws  IllegalStateException
        *          if this method is invoked before the given DMO interface has been proxied.
        */
       public static Map<String, Method> legacyGettersByProperty(Class<? extends DataModelObject> dmoIface)
       {
          return getMapFailOnNull(dmoIface, p2lgCache);
       }

       /**
        * Cache an immutable map of DMO property names to getter methods for the given DMO type,
        * keyed by DMO interface.
        * <p>
        * This method is only intended to be called once per DMO interface. The map for a given DMO
        * interface can only be stored once. Further requests to store a new map for that interface
        * will fail silently and the original mapping will remain.
        *
        * @param   dmoIface
        *          DMO interface.
        * @param   map
        *          A map as described above.
        */
       static void putLegacyGettersByProperty(Class<? extends DataModelObject> dmoIface, Map<String, Method> map)
       {
          p2lgCache.putIfAbsent(dmoIface, storableMap(map));
       }

       /**
        * Get an immutable map of DMO property names to setter methods for the given DMO type.
        * <p>
        * <strong>WARNING:</strong> a mapping for any given DMO type is only available after the
        * first time a record buffer is defined for that DMO type. The information is collected and
        * stored when a DMO interface is first proxied, so this method must not be invoked before
        * that occurs. To do so represents a programming error and will raise an exception.
        *
        * @param   dmoIface
        *          DMO interface.
        *
        * @return  A map as described above.
        *
        * @throws  IllegalStateException
        *          if this method is invoked before the given DMO interface has been proxied.
        */
       public static Map<String, Method> legacySettersByProperty(Class<? extends DataModelObject> dmoIface)
       {
          return getMapFailOnNull(dmoIface, p2lsCache);
       }

       /**
        * Cache an immutable map of DMO property names to setter methods for the given DMO type,
        * keyed by DMO interface.
        * <p>
        * This method is only intended to be called once per DMO interface. The map for a given DMO
        * interface can only be stored once. Further requests to store a new map for that interface
        * will fail silently and the original mapping will remain.
        *
        * @param   dmoIface
        *          DMO interface.
        * @param   map
        *          A map as described above.
        */
       static void putLegacySettersByProperty(Class<? extends DataModelObject> dmoIface, Map<String, Method> map)
       {
          p2lsCache.putIfAbsent(dmoIface, storableMap(map));
       }

       /**
        * Cache an immutable map of DMO property names to bulk getter methods for the given DMO type,
        * keyed by DMO interface.
        * <p>
        * This method is only intended to be called once per DMO interface. The map for a given DMO
        * interface can only be stored once. Further requests to store a new map for that interface
        * will fail silently and the original mapping will remain.
        *
        * @param   dmoIface
        *          DMO interface.
        * @param   map
        *          A map as described above.
        */
       static void putLegacyBulkGettersByProperty(Class<? extends DataModelObject> dmoIface, Map<String, Method> map)
       {
          bp2lgCache.putIfAbsent(dmoIface, storableMap(map));
       }

       /**
        * Cache an immutable map of DMO property names to bulk setter methods for the given DMO type,
        * keyed by DMO interface.
        * <p>
        * This method is only intended to be called once per DMO interface. The map for a given DMO
        * interface can only be stored once. Further requests to store a new map for that interface
        * will fail silently and the original mapping will remain.
        *
        * @param   dmoIface
        *          DMO interface.
        * @param   map
        *          A map as described above.
        */
       static void putLegacyBulkSettersByProperty(Class<? extends DataModelObject> dmoIface, Map<String, Method> map)
       {
          bp2lsCache.putIfAbsent(dmoIface, storableMap(map));
       }

       /**
        * Get an immutable map of DMO property names to bulk legacy getter methods for the given DMO
        * type.
        * <p>
        * <strong>WARNING:</strong> a mapping for any given DMO type is only available after the
        * first time a record buffer is defined for that DMO type. The information is collected and
        * stored when a DMO interface is first proxied, so this method must not be invoked before
        * that occurs. To do so represents a programming error and will raise an exception.
        *
        * @param   dmoIface
        *          DMO interface.
        *
        * @return  A map as described above.
        *
        * @throws  IllegalStateException
        *          if this method is invoked before the given DMO interface has been proxied.
        */
       static Map<String, Method> bulkLegacyGettersByProperty(Class<? extends DataModelObject> dmoIface)
       {
          return getMapFailOnNull(dmoIface, bp2lgCache);
       }

       /**
        * Get an immutable map of DMO property names to bulk legacy getter methods for the given DMO
        * type.
        * <p>
        * <strong>WARNING:</strong> a mapping for any given DMO type is only available after the
        * first time a record buffer is defined for that DMO type. The information is collected and
        * stored when a DMO interface is first proxied, so this method must not be invoked before
        * that occurs. To do so represents a programming error and will raise an exception.
        *
        * @param   dmoIface
        *          DMO interface.
        *
        * @return  A map as described above.
        *
        * @throws  IllegalStateException
        *          if this method is invoked before the given DMO interface has been proxied.
        */
       static Map<String, Method> bulkLegacySettersByProperty(Class<? extends DataModelObject> dmoIface)
       {
          return getMapFailOnNull(dmoIface, bp2lsCache);
       }

       /**
        * Get an map of legacy getter names to their corresponding property names for the given DMO
        * type. This is the inversion of the map provided by {@link #legacyGettersByProperty(Class)}.
        * <p>
        * <strong>WARNING:</strong> a mapping for any given DMO type is only available after the
        * first time a record buffer is defined for that DMO type. The information is collected and
        * stored when a DMO interface is first proxied, so this method must not be invoked before
        * that occurs. To do so represents a programming error and will raise an exception.
        *
        * @param   dmoIface
        *          DMO interface for which the map is desired.
        *
        * @return  Map as described above.
        */
       static Map<Method, String> legacyPropertiesByGetter(Class<? extends DataModelObject> dmoIface)
       {
          return propertiesByMethod(dmoIface, g2lCache, PropertyHelper::legacyGettersByProperty);
       }

       /**
        * Get an map of legacy setter names to their corresponding property names for the given DMO
        * type. This is the inversion of the map provided by {@link #legacySettersByProperty(Class)}.
        * <p>
        * <strong>WARNING:</strong> a mapping for any given DMO type is only available after the
        * first time a record buffer is defined for that DMO type. The information is collected and
        * stored when a DMO interface is first proxied, so this method must not be invoked before
        * that occurs. To do so represents a programming error and will raise an exception.
        *
        * @param   dmoIface
        *          DMO interface for which the map is desired.
        *
        * @return  Map as described above.
        */
       static Map<Method, String> legacyPropertiesBySetter(Class<? extends DataModelObject> dmoIface)
       {
          return propertiesByMethod(dmoIface, s2lCache, PropertyHelper::legacySettersByProperty);
       }

       /**
        * Get an map of POJO getter methods to their corresponding property names for the given DMO
        * type. This is the inversion of the map provided by {@link #pojoGettersByProperty(Class)}.
        *
        * @param   dmoIface
        *          DMO interface for which the map is desired.
        *
        * @return  Map as described above.
        */
       static Map<Method, String> pojoPropertiesByGetter(Class<? extends DataModelObject> dmoIface)
       {
          return propertiesByMethod(dmoIface, g2pCache, PropertyHelper::pojoGettersByProperty);
       }

       /**
        * Get an map of POJO setter methods to their corresponding property names for the given DMO
        * type. This is the inversion of the map provided by {@link #pojoSettersByProperty(Class)}.
        *
        * @param   dmoIface
        *          DMO interface for which the map is desired.
        *
        * @return  Map as described above.
        */
       static Map<Method, String> pojoPropertiesBySetter(Class<? extends DataModelObject> dmoIface)
       {
          return propertiesByMethod(dmoIface, s2pCache, PropertyHelper::pojoSettersByProperty);
       }

       /**
        * Build a map of all methods in the given array, whose names begin with any of the given
        * prefixes. The keys are property names, derived by discarding a matching prefix from the
        * method name, and decapitalizing the first letter of the remaining text. The values are the
        * methods.
        *
        * @param   methods
        *          Methods to be tested.
        * @param   prefix
        *          Array of prefix strings which are used to identify matching methods.
        *
        * @return  Map of DMO property names to matching methods.
        */
       static Map<String, Method> methodsByProperty(Method[] methods, String[] prefix)
       {
          // Always prefer indexed methods (specifically for setters, which have other variants).
          Set<String> indexed = new HashSet<>();

          Map<String, Method> map = new HashMap<>();
          int len = methods.length;
          for (int i = 0; i < len; i++)
          {
             Method next = methods[i];
             next.setAccessible(true);
             String name = next.getName();

             // special handling for primaryKey() getter and primaryKey(Long) setter methods
             if (primaryKey.equals(name))
             {
                boolean setterTest = prefix.length == 1 && "set".equals(prefix[0]);
                boolean setterMeth = next.getParameterTypes().length == 1;

                if (setterTest == setterMeth)
                {
                   map.put(Session.PK, next);
                }

                continue;
             }

             for (int j = 0; j < prefix.length; j++)
             {
                String pfx = prefix[j];
                if (name.startsWith(pfx))
                {
                   String rootName = makePropertyName(name, pfx.length()).intern();

                   // If we've already stored an indexed version, we don't want to replace it.
                   if (indexed.contains(rootName))
                   {
                      continue;
                   }

                   map.put(rootName, next);

                   // Check whether first parameter is an index (int).
                   Class<?>[] argTypes = next.getParameterTypes();
                   if (argTypes.length > 0 && Integer.TYPE.equals(argTypes[0]))
                   {
                      indexed.add(rootName);
                   }

                   break;
                }
             }
          }

          return map;
       }

       /**
        * Get a list of methods which, when invoked, will trigger Hibernate to initialize the lazily
        * initialized persistent lists which are used to manage extent fields for a DMO.  If a DMO
        * has no extent fields, an empty list is returned.
        *
        * @param   recordBuffer
        *          The record buffer to introspect.
        *
        * @return  A list of trigger methods or an empty list.
        *
        * @deprecated is this still used?
        */
       @Deprecated
       static List<Method> lazyExtentTriggersForInterface(RecordBuffer recordBuffer)
       {
          Class<? extends DataModelObject> dmoIface = recordBuffer.getDMOInterface();
          List<Method> list = i2ltCache.get(dmoIface);

          if (list == null)
          {
             Map<String, Method> getters = legacyGettersByProperty(dmoIface);
             Map<Integer, Method> gettersBySize = new HashMap<>();
             Iterator<Map.Entry<String, Integer>> iter =
                extentsByProperty(dmoIface).entrySet().iterator();
             while (iter.hasNext())
             {
                Map.Entry<String, Integer> next = iter.next();
                String property = next.getKey();

                // exclude denormalized extent fields
                if (TableMapper.getDenormalizedProperty(recordBuffer, property, 0) != null)
                {
                   continue;
                }

                Integer extent = next.getValue();
                Method getter = getters.get(property);
                Method bestSoFar = gettersBySize.get(extent);
                if (bestSoFar == null)
                {
                   gettersBySize.put(extent, getter);
                }
                else
                {
                   Class<?> bestTypeSoFar = bestSoFar.getReturnType();
                   if (integer.class.equals(bestTypeSoFar) || logical.class.isAssignableFrom(bestTypeSoFar))
                   {
                      // Can't improve on logical or integer; their copy constructors are the
                      // fastest.
                      continue;
                   }
                   Class<?> type = getter.getReturnType();
                   if (decimal.class.equals(bestTypeSoFar) && !decimal.class.equals(type))
                   {
                      // Anything is an improvement over a decimal return type, which is the
                      // slowest to construct.
                      gettersBySize.put(extent, getter);
                      continue;
                   }
                }
             }

             if (gettersBySize.isEmpty())
             {
                list = Collections.emptyList();
             }
             else
             {
                list = new ArrayList<>(gettersBySize.values());
                list = Collections.unmodifiableList(list);
             }

             List<Method> existing = i2ltCache.putIfAbsent(dmoIface, list);
             if (existing != null)
             {
                list = existing;
             }
          }

          return list;
       }

       /**
        * Given a method name ending in a property name, extract the property
        * name by trimming off the prefix and decapitalizing the root name, using
        * <code>java.beans.Introspector.decapitalize()</code>.  Note that this
        * method will not decapitalize the first letter of the name if both the
        * first letter and the second letter (if any) are both uppercase.  This
        * quirk is necessary for compatibility with Hibernate's internal property
        * name conversions.
        *
        * @param   methodName
        *          Method name ending in the property name.
        * @param   index
        *          Index of first character of property name.
        *
        * @return  Property name.
        */
       static String makePropertyName(String methodName, int index)
       {
          String baseName = methodName.substring(index);
          String propName = Introspector.decapitalize(baseName);

          return propName;
       }

       /**
        * Report whether the given method is getter or setter.
        *
        * @param   method
        *          Method.
        *
        * @return  <code>true</code> if method is getter or setter otherwise <code>false</code>.
        */
       static boolean isJavaBeanMethod(Method method)
       {
          String methodName = method.getName();

          return methodName.startsWith("get") ||
                 methodName.startsWith("set") ||
                 methodName.startsWith("is");
       }

       /**
        * Get a map of DMO property names to POJO getter/setter methods for the given DMO type. POJO
        * methods are those that are declared only by the top-level DMO interface, excluding those
        * declared by the enclosed, DMO buffer sub-interface.
        *
        * @param   dmoIface
        *          DMO interface (top level) for which the map is desired. If no mapping is found in
        *          the <code>cache</code> and this interface has a DMO interface as a superinterface,
        *          the hierarchy is walked up to the furthest ancestor, and that interface is used.
        * @param   cache
        *          Cache which is searched for the map first, and to which the new map is added if
        *          built.
        * @param   methodPrefixes
        *          One or more method name prefixes which will identify the method as a getter or a
        *          setter.
        *
        * @return  A map as described above.
        */
       private static
       Map<String, Method> pojoMethodsByProperty(Class<?> dmoIface,
                                                 Map<Class<?>, Map<String, Method>> cache,
                                                 String[] methodPrefixes)
       {
          Map<String, Method> map = cache.get(dmoIface);

          if (map == null)
          {
             // try looking it up under the "root" interface, if different
             Class<?> root = getRootPojoInterface(dmoIface);
             boolean descendant = !root.equals(dmoIface);
             if (descendant)
             {
                map = cache.get(root);
             }

             // if still null, populate a map and store it
             if (map == null)
             {
                Method[] methods = root.getDeclaredMethods();
                map = methodsByProperty(methods, methodPrefixes);
                map = storableMap(map);

                // current value should be null unless we had a race condition
                Map<String, Method> existing = cache.putIfAbsent(root, map);
                if (existing != null)
                {
                   map = existing;
                }

                if (descendant)
                {
                   // make sure the same map is stored under both keys to avoid duplicates
                   existing = cache.putIfAbsent(dmoIface, map);
                   if (existing != null)
                   {
                      map = existing;
                   }
                }
             }
          }

          return map;
       }

       /**
        * Get a map of DMO property names to all legacy and POJO getter or setter methods, for the
        * given DMO type.
        *
        * @param   dmoIface
        *          DMO interface (top level) for which the map is desired. If no mapping is found in
        *          the <code>allCache</code> and this interface has a DMO interface as a
        *          superinterface, the hierarchy is walked up to the furthest ancestor, and that
        *          interface is used.
        * @param   allCache
        *          Cache which is searched for the map first, and to which the new map is added if
        *          built.
        * @param   legacyCache
        *          Cache which is searched for the legacy method elements of the map.
        * @param   pojoCache
        *          Cache which is searched for the POJO method elements of the map.
        *
        * @return  A map as described above.
        *
        * @see     #allGettersByProperty(Class)
        * @see     #allSettersByProperty(Class)
        */
       private static
       Map<String, Method> allMethodsByProperty(Class<?> dmoIface,
                                                Map<Class<?>, Map<String, Method>> allCache,
                                                Map<Class<?>, Map<String, Method>> legacyCache,
                                                Map<Class<?>, Map<String, Method>> pojoCache,
                                                String[] methodPrefixes)
       {
          Map<String, Method> map = allCache.get(dmoIface);

          if (map == null)
          {
             // try looking it up under the "root" interface, if different
             Class<?> root = getRootPojoInterface(dmoIface);
             boolean descendant = !root.equals(dmoIface);
             if (descendant)
             {
                map = allCache.get(root);
             }

             // if still null, populate a map and store it
             if (map == null)
             {
                // merge legacy and pojo into one map
                Map<String, Method> legacy = getMapFailOnNull(dmoIface, legacyCache);
                Map<String, Method> pojo = pojoMethodsByProperty(dmoIface, pojoCache, methodPrefixes);
                map = new HashMap<>(legacy);
                map.putAll(pojo);
                map = storableMap(map);

                // current value should be null unless we had a race condition
                Map<String, Method> existing = allCache.putIfAbsent(root, map);
                if (existing != null)
                {
                   map = existing;
                }

                if (descendant)
                {
                   // make sure the same map is stored under both keys to avoid duplicates
                   existing = allCache.putIfAbsent(dmoIface, map);
                   if (existing != null)
                   {
                      map = existing;
                   }
                }
             }
          }

          return map;
       }

       /**
        * Given a top-level DMO interface, inspect its hierarchy of super-interfaces and return the
        * furthest ancestor interface from the given interface (the "root"), which is still a DMO
        * interface.
        * <p>
        * For purposes of this algorithm, a DMO interface is defined as one which directly extends
        * {@link Temporary} or {@link DataModelObject}. Note that the starting interface may already
        * be the root.
        *
        * @param   dmoIface
        *          Starting DMO interface.
        *
        * @return  Root, top-level DMO interface.
        */
       private static Class<?> getRootPojoInterface(Class<?> dmoIface)
       {
          if (Persistable.class.equals(dmoIface))
          {
             return dmoIface;
          }

          if (!dmoIface.isInterface() || Buffer.class.isAssignableFrom(dmoIface))
          {
             throw new IllegalArgumentException("Expected DMO POJO interface; found: " + dmoIface);
          }

          Class<?> next = dmoIface;
          Class<?>[] parents;
          for (Class<?> parent : parents = next.getInterfaces())
          {
             if (parents.length > 1)
             {
                throw new IllegalArgumentException("Invalid DMO POJO interface: " + next);
             }

             if (Temporary.class.equals(parent) || DataModelObject.class.equals(parent))
             {
                break;
             }
             else
             {
                next = parent;
             }
          }

          return next;
       }

       /**
        * Get a map of DMO methods to the property names with which they are associated. First, the
        * provided cache is checked. If the map is not available there, it is created using the given
        * worker method, then cached for future reference.
        *
        * @param   dmoIface
        *          DMO interface with which the map is associated and cached.
        * @param   cache
        *          Cache containing maps of the desired type, keyed by DMO interface.
        * @param   worker
        *          Worker method which can be used to get a map of DMO properties to methods of the
        *          desired type. The returned map will be {@link #invertMap(Map, Map) inverted} and
        *          the inverted copy cached.
        *
        * @return  A map as described above.
        */
       private static
       Map<Method, String> propertiesByMethod(Class<? extends DataModelObject> dmoIface,
                                              Map<Class<?>, Map<Method, String>> cache,
                                              Function<Class<? extends DataModelObject>, Map<String, Method>> worker)
       {
          Map<Method, String> map = cache.get(dmoIface);
          if (map == null)
          {
             map = invertMap(worker.apply(dmoIface), new HashMap<>());
             Map<Method, String> existing = cache.putIfAbsent(dmoIface, storableMap(map));
             if (existing != null)
             {
                map = existing;
             }
          }

          return map;
       }

       /**
        * Retrieve a map from a cache of maps, keyed by DMO interface. The maps are put into the
        * cache as DMO interfaces are first proxied, so this method will fail if the requested
        * information has not yet been collected.
        *
        * @param   dmoIface
        *          DMO interface, which is the key to the desired map.
        * @param   cache
        *          Cache from which to retrieve the map.
        *
        * @return  The requested map.
        *
        * @throws  IllegalStateException
        *          if this method is invoked before the given DMO interface has been proxied.
        */
       private static <K, V>
       Map<K, V> getMapFailOnNull(Class<?> dmoIface, Map<Class<?>, Map<K, V>> cache)
       {
          Map<K, V> map = cache.get(dmoIface);

          if (map == null)
          {
             throw new IllegalStateException(
                "Information has not yet been collected for DMO interface: " +
                dmoIface.getName());
          }

          return map;
       }

       /**
        * Get a map that is suitable for storing in one of this class' caches, given an existing map.
        * If the given map is <code>null</code> or empty, return a singleton empty map; otherwise,
        * return an unmodifiable view of the given map.
        *
        * @param   map
        *          Original map, or <code>null</code>.
        *
        * @return  A map as described above.
        */
       private static <K,V> Map<K,V> storableMap(Map<K, V> map)
       {
          if (map == null || map.isEmpty())
          {
             return Collections.emptyMap();
          }

          return Collections.unmodifiableMap(map);
       }

       /**
        * Make a copy of the given map where the values of the original are the keys of the copy,
        * and the keys of the original are the values of the copy. Note that this algorithm presumes
        * a one-to-one relationship between keys and values; otherwise, entries from the original
        * will be lost in an undefined way. It further presumes that the type of the original map's
        * values are suitable as unique keys.
        *
        * @param   original
        *          Original map.
        * @param   inverted
        *          Copy map with keys and values of the original inverted.
        *
        * @return  A map as described above.
        */
       private static <K, V> Map<V, K> invertMap(Map<K, V> original, Map<V, K> inverted)
       {
          for (Map.Entry<K, V> entry : original.entrySet())
          {
             K key = entry.getKey();
             V value = entry.getValue();
             inverted.put(value, key);
          }

          return inverted;
       }
    }