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