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