DmoProxyPlugin.java

/*
** Module   : DmoProxyPlugin.java
** Abstract : A ProxyAssemblerPlugin implementation for DMOs.
**
** Copyright (c) 2016-2024, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
** 001 ECF 20160219 Created initial version. Simplifies and moves a significant amount of logic
**                  from the RecordBuffer invocation handler into a much more performant
**                  implementation in a DMO proxy.
** 002 EVL 20160325 Javadoc fixes.
** 003 ECF 20171025 Javadoc correction.
** 004 ECF 20171231 Performance improvement.
** 005 OM  20180614 Saved arrayBulk(S/G)etterMap s to PropertyHelper.
** 006 CA  20190812 Made analyze() package-private - is required to allow TempTableBuilder to
**                  initialize PropertyHelper, if a certain legacy table has not been loaded in
**                  FWD yet, but is referenced by its name.
** 007 OM  20191115 New ORM implementation.
** 008 OM  20201001 Improved DMO manipulation performance by caching slow Property annotation access.
** 009 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 010 AD  20240321 Modified class to permit creation and use of this plugin at conversion/compilation step.
** 011 ICP 20240715 Modified analyze() to also search methods implemented in the hierarchy.
*/

/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
** 
** Additional terms under GNU Affero GPL version 3 section 7:
** 
**   Under Section 7 of the GNU Affero GPL version 3, the following additional
**   terms apply to the works covered under the License.  These additional terms
**   are non-permissive additional terms allowed under Section 7 of the GNU
**   Affero GPL version 3 and may not be removed by you.
** 
**   0. Attribution Requirement.
** 
**     You must preserve all legal notices or author attributions in the covered
**     work or Appropriate Legal Notices displayed by works containing the covered
**     work.  You may not remove from the covered work any author or developer
**     credit already included within the covered work.
** 
**   1. No License To Use Trademarks.
** 
**     This license does not grant any license or rights to use the trademarks
**     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
**     of Golden Code Development Corporation. You are not authorized to use the
**     name Golden Code, FWD, or the names of any author or contributor, for
**     publicity purposes without written authorization.
** 
**   2. No Misrepresentation of Affiliation.
** 
**     You may not represent yourself as Golden Code Development Corporation or FWD.
** 
**     You may not represent yourself for publicity purposes as associated with
**     Golden Code Development Corporation, FWD, or any author or contributor to
**     the covered work, without written authorization.
** 
**   3. No Misrepresentation of Source or Origin.
** 
**     You may not represent the covered work as solely your work.  All modified
**     versions of the covered work must be marked in a reasonable way to make it
**     clear that the modified work is not originating from Golden Code Development
**     Corporation or FWD.  All modified versions must contain the notices of
**     attribution required in this license.
*/

package com.goldencode.p2j.persist;

import java.lang.reflect.*;
import java.util.*;
import java.util.logging.*;
import com.goldencode.p2j.persist.orm.*;
import com.goldencode.asm.AsmUtils;
import com.goldencode.p2j.schema.*;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.logging.*;
import com.goldencode.proxy.*;
import org.objectweb.asm.*;
import org.apache.commons.lang3.tuple.Pair;

/**
 * A proxy assembler plugin which manages the assembly of indexed DMO getter/setter methods,
 * for normalized and denormalized implementations. The proxy assembler is allowed to perform
 * its default assembly of all simple, scalar getter and setter methods. This plugin handles the
 * full assembly of the following method types (where "foo" is an example property method):
 * <pre>
 *    T&lt;? extends BaseDataType&gt;   getFoo(NumberType index)
 *    T&lt;? extends BaseDataType&gt;[] getFoo()
 *                         void   setFoo(NumberType index, T&lt;? extendsBaseDataType&gt; foo)
 *                         void   setFoo(T&lt;? extendsBaseDataType&gt; foo)
 *                         void   setFoo(T&lt;? extendsBaseDataType&gt;[] foo)
 * </pre>
 * <p>
 * Note that the supported return and parameter types are concrete subclasses of
 * <code>BaseDataType</code>, not generics; the generics above are meant only to express that the
 * data types supported are limited to <code>BaseDataType</code> sub-types.
 * <p>
 * In addition, methods with the following signatures are handled specially by this plugin:
 * <pre>
 *    T&lt;? extends BaseDataType&gt;   getFoo(int index)
 *                         void   setFoo(int index, T&lt;? extendsBaseDataType&gt; foo)
 * </pre>
 * <p>
 * For extent fields converted to a normalized form, the above two method types are overridden
 * by this plugin. For the most part, the implementation is provided by the main proxy assembler.
 * However, it is augmented with a range check, which is applied before the normal delegation to
 * an invocation handler.
 * <p>
 * For the denormalized form of these two method types, the plugin implementation applies the
 * range check, then performs the bytecode equivalent of a switch statement to delegate an indexed
 * invocation to the appropriate, matching scalar getter or setter method. The actual scalar
 * method invoked is determined by the subscript value passed into the indexed method. The scalar
 * method then delegates to the invocation handler as usual.
 * <p>
 * As a result of these specialized method implementations, the only DMO getter and setter method
 * signatures that ever are passed to the associated invocation handler for scalar fields and
 * extent fields converted to a denormalized form are simple, scalar variants (simple bean
 * methods). For extent fields converted to a normalized form, the additional two, indexed methods
 * above are passed in as well. This allows the invocation handler to remain simpler and avoid
 * expensive runtime lookups to distinguish the less commonly invoked forms of the legacy-support
 * methods (i.e., the first five variants listed above). It also ensures that any subscripts have
 * been checked for unknown value (for {@code NumberType} index values) and for range
 * validity before the invocation handler is invoked. This avoids the runtime cost of determining
 * extents for subscript validity checks in the invocation handler, since these checks are now
 * compiled into the proxies themselves.
 * 
 * @author ecf
 */
public final class DmoProxyPlugin
implements ProxyAssemblerPlugin,
           DmoAsmTypes,
           Opcodes
{
   /** Logger */
   private static final CentralLogger LOG = CentralLogger.get(DmoProxyPlugin.class.getName());
   
//   /** Prefix for name of composite inner class containing normalized extent field properties */
//   private static final String COMPOSITE = "Composite";
   
   /** The DMO interface. */
   private final Class<? extends DataModelObject> dmoIface;
   
   /** DMO interface which declares POJO bean, <code>Buffer</code> and legacy support methods */
   private final Class<? extends DataModelObject /*& Buffer*/> targetInterface;
   
   /** Methods for which this plugin implements proxies (instead of default proxy assembler) */
   private Set<Method> methods = null;
   
   /** Indexed getter/setter methods to proxy method assemblers */
   private Map<Method, ProxyAssembler.ProxyMethod> methodAssemblerMap = null;
   
   /** Indexed property names to extent sizes (does not include denormalized properties) */
   private Map<String, Integer> extentMap = new HashMap<>();
   
   /** Set of property names representing only legacy fields (not denormalized) */
   private Set<String> legacyProps = new HashSet<>();
   
   /** Property names to legacy getter methods (primitive int index for extent fields) */
   private Map<String, Method> legacyGetterMap = new LinkedHashMap<>();
   
   /** Property names to legacy setter methods (primitive int index for extent fields) */
   private Map<String, Method> legacySetterMap = new LinkedHashMap<>();
   
   /** Indexed property names to arrays of denormalized, custom extent property names */
   private Map<String, String[]> denormPropMap = new LinkedHashMap<>();
   
   /** Denormalized properties mapped to original properties and  their index (1-base). */
   private Map<String, Pair<String, Integer>> reverseDenormPropMap = new HashMap<>();
   
   /** Indexed property names to corresponding extents */
   private Map<String, Integer> normPropExtentMap = new LinkedHashMap<>();
   
   /** Property names to POJO getter methods */
   private Map<String, Method> pojoGetterMap = null;
   
   /** Property names to all setter methods */
   private Map<String, Method> pojoSetterMap = null;
   
   /** Denormalized property names to indexed getter methods with a primitive integer index. */
   private Map<String, Method> primGetterMap = new LinkedHashMap<>();
   
   /** Denormalized property names to indexed setter methods with a primitive integer index. */
   private Map<String, Method> primSetterMap = new LinkedHashMap<>();
   
   /** Denormalized property names to indexed getter methods with a NumberType wrapper index */
   private Map<String, Method> bdtGetterMap = new LinkedHashMap<>();
   
   /** Denormalized property names to indexed setter methods with a NumberType wrapper index */
   private Map<String, Method> bdtSetterMap = new LinkedHashMap<>();
   
   /** Denormalized property names to getter methods which return an array of wrapper objects */
   private Map<String, Method> arrayBulkGetterMap = new LinkedHashMap<>();
   
   /** Denormalized property names to bulk setter methods which accept an array parameter */
   private Map<String, Method> arrayBulkSetterMap = new LinkedHashMap<>();
   
   /** Denormalized property names to bulk setter methods which accept a scalar parameter */
   private Map<String, Method> scalarBulkSetterMap = new LinkedHashMap<>();
   
   /** Indicates whether exceptions from {@link DmoMetadataManager} should be caught. */
   private boolean catchDmoMetaException = false;
   
   /**
    * Create an instance of this plugin. An instance is created each time a DMO is proxied by
    * the main proxy assembler. This instance should be short-lived and discarded once its work
    * is done.
    *
    * @param   dmoBufInterface
    *          DMO buffer interface.
    * @param   dmoInterface
    *          DMO interface.
    */
   public DmoProxyPlugin(Class<?> dmoBufInterface,
                         Class<? extends DataModelObject> dmoInterface)
   {
      this.targetInterface = findRootProxyInterface(dmoBufInterface);
      this.dmoIface = dmoInterface;
   }
   
   /**
    * Create an instance of this plugin. An instance is created each time a DMO is proxied by
    * the main proxy assembler. This instance should be short-lived and discarded once its work
    * is done.
    * It might encounter exceptions from the {@link DmoMetadataManager} when analyzing the fields, but be meant to
    * catch them and continue execution.
    *
    * @param   dmoBufInterface
    *          DMO buffer interface.
    * @param   dmoInterface
    *          DMO interface.
    * @param   catchDmoMetaException
    *          True if exception from the {@link DmoMetadataManager} should be caught and handled
    *          when analyzing the fields.
    */
   public DmoProxyPlugin(Class<?> dmoBufInterface,
                         Class<? extends DataModelObject> dmoInterface,
                         boolean catchDmoMetaException)
   {
      this.catchDmoMetaException = catchDmoMetaException;
      this.targetInterface = findRootProxyInterface(dmoBufInterface);
      this.dmoIface = dmoInterface;
   }
   
   /**
    * Find the ancestral root of inner interfaces which extend {@code Buffer}. This will be
    * the one which declares any special methods declared for legacy business logic, and whose
    * enclosing interface defines the POJO methods of the DMO.
    * <p>
    * Converted temp-table DMO interfaces may define such nested interfaces arbitrarily deeply,
    * so we have to find the root in order to inspect the methods we need to proxy.
    * 
    * @param   iface
    *          DMO buffer interface to test.
    * 
    * @return  Ancestral, inner, DMO buffer interface, or {@code iface} if it represents the
    *          root.
    */
   private static Class<? extends DataModelObject> findRootProxyInterface(Class<?> iface)
   {
      Class<?> next = iface;
      for (Class<?> parent : next.getInterfaces())
      {
         if (!Buffer.class.equals(parent) && Buffer.class.isAssignableFrom(parent))
         {
            if (parent.getEnclosingClass() != null)
            {
               next = findRootProxyInterface(parent);
            }
            else
            {
               break;
            }
         }
      }
   
      if (next == null || !DataModelObject.class.isAssignableFrom(next))
      {
         return null; // maybe throw some exceptions (?)
      }
   
      return (Class<? extends DataModelObject>) next;
   }
   
   /**
    * Assemble bytecode instructions to return an instance of a <code>BaseDataType</code>
    * subclass of the given type, initialized to unknown value. While the call to
    * <code>setUnknownValue()</code> may be redundant for most BDT sub-types (since a default
    * constructor initializes to unknown value in most cases), it was decided to leave this
    * invocation in place to make the operation explicit and to make the assembled code more
    * resilient to possible future changes.
    * 
    * @param   mv
    *          Method visitor.
    * @param   typeName
    *          Internal form name of <code>BaseDataType</code> subclass which should be
    *          instantiated.
    * @param   localVar
    *          Zero-based index of the local variable slot which should be used for the unknown
    *          value object before the object is returned.
    */
   private static void assembleReturnUnknown(MethodVisitor mv, String typeName, int localVar)
   {
      // instantiate an object of the method's return type (a BDT subclass)
      mv.visitTypeInsn(NEW, typeName);
      
      // duplicate the reference
      mv.visitInsn(DUP);
      
      // invoke the default c'tor
      mv.visitMethodInsn(INVOKESPECIAL, typeName, METH_INIT, SIG_DEF_INIT, false);
      
      // store the reference in a local variable
      mv.visitVarInsn(ASTORE, localVar);
      
      // load it back from the variable
      mv.visitVarInsn(ALOAD, localVar);
      
      // call setUnknown on the BDT instance
      mv.visitMethodInsn(INVOKEVIRTUAL, typeName, METH_SETUNK, SIG_SETUNK, false);
      
      // load it back from the variable
      mv.visitVarInsn(ALOAD, localVar);
      
      // return it
      mv.visitInsn(ARETURN);
   }
   
   /**
    * Report the set of methods which this plugin will implement. This set will not be
    * implemented by the main proxy assembler. This method is invoked before the main proxy
    * assembler begins assembling the proxy class.
    * <p>
    * Note that any method in the returned set will NOT be included in the array of methods the
    * invocation handler will have access to at runtime. Therefore, the returned set should NOT
    * include any method handled specifically by {@link #getProxyMethodAssembler(Method)}.
    * {@link #getProxyMethodAssembler(Method)} will not be invoked for any of the methods in the
    * returned set.
    * <p>
    * See the {@link DmoProxyPlugin class javadoc} for additional information on which DMO
    * methods this plugin will implement.
    * 
    * @return  Set of methods which this plugin will implement.
    */
   @Override
   public Set<Method> getImplementedMethods()
   {
      if (methods == null)
      {
         analyze();
      }
      
      return methods;
   }
   
   /**
    * Return an object which will assemble the given proxy method, or <code>null</code> if this
    * plugin will allow the default proxy method implementation. If a non-<code>null</code>
    * value is returned, the corresponding method should NOT be included in the set of methods
    * reported by {@link #getImplementedMethods()}.
    * <p>
    * This method is invoked for each proxy method implemented by the main proxy assembler.
    * <p>
    * This plugin implementation will return a non-<code>null</code> value only for indexed
    * getter and setter fields which accept a primitive <code>int</code> for the first argument
    * (the subscript value), and only in the case the associated extent field was converted in 
    * normalized form. This is done because we need the default proxy implementation for this
    * category of method (it is backed by an implementation in the DMO implementation class), but
    * we also need to implement a range check on the subscript value ahead of that default
    * behavior.
    * <p>
    * For all other methods, <code>null</code> is returned.
    * 
    * @param   method
    *          Method for which a proxy implementation is needed.
    * 
    * @return  A subclass of <code>ProxyAssembler.ProxyMethod</code> or <code>null</code> if the
    *          default proxy method implementation is suitable for the given method.
    * 
    * @see     #getImplementedMethods()
    */
   @Override
   public ProxyAssembler.ProxyMethod getProxyMethodAssembler(Method method)
   {
      return methodAssemblerMap == null ? null : methodAssemblerMap.get(method);
   }
   
   /**
    * Entry point into this plugin's arbitrary method assembly process. This will be called after
    * {@link #getImplementedMethods()} and after {@link #getProxyMethodAssembler(Method)} (i.e.,
    * after the main proxy assembler has implemented its methods), but before
    * <code>ClassWriter.visitEnd</code> is invoked to terminate the assembly process.
    * 
    * @param   classWriter
    *          ASM class writer which the plugin will use to do its work.
    */
   @Override
   public void assemble(ClassWriter classWriter)
   {
      if (methods == null)
      {
         analyze();
      }
      
      String targetIfaceType = AsmUtils.commonToInternalTypeName(targetInterface);
      
      for (Map.Entry<String, Method> entry : primGetterMap.entrySet())
      {
         String property = entry.getKey();
         Method method = entry.getValue();
         assembleDenormPrimGetter(classWriter, targetIfaceType, property, method);
      }
      
      for (Map.Entry<String, Method> entry : primSetterMap.entrySet())
      {
         String property = entry.getKey();
         Method method = entry.getValue();
         assembleDenormPrimSetter(classWriter, targetIfaceType, property, method);
      }
      
      for (Map.Entry<String, Method> entry : bdtGetterMap.entrySet())
      {
         String property = entry.getKey();
         Method method = entry.getValue();
         assembleDenormBdtGetter(classWriter, targetIfaceType, property, method);
      }
      
      for (Map.Entry<String, Method> entry : bdtSetterMap.entrySet())
      {
         String property = entry.getKey();
         Method method = entry.getValue();
         assembleDenormBdtSetter(classWriter, targetIfaceType, property, method);
      }
      
      for (Map.Entry<String, Method> entry : arrayBulkGetterMap.entrySet())
      {
         String property = entry.getKey();
         Method method = entry.getValue();
         assembleArrayBulkGetter(classWriter, targetIfaceType, property, method);
      }
      
      for (Map.Entry<String, Method> entry : arrayBulkSetterMap.entrySet())
      {
         String property = entry.getKey();
         Method method = entry.getValue();
         assembleArrayBulkSetter(classWriter, targetIfaceType, property, method);
      }
      
      for (Map.Entry<String, Method> entry : scalarBulkSetterMap.entrySet())
      {
         String property = entry.getKey();
         Method method = entry.getValue();
         assembleScalarBulkSetter(classWriter, targetIfaceType, property, method);
      }
   }
   
   /**
    * Analyze the instance fields in the DMO implementation class associated with the DMO
    * interface, and cross-reference this information with the interface's methods, for the
    * purpose of assembling proxy implementations of these methods.
    * <p>
    * Update the {@link PropertyHelper}'s legacy getter and setter method caches for the DMO
    * interface as an important side effect.
    */
   void analyze()
   {
      methods = new HashSet<>();
      
      analyzeFields(dmoIface);
      
      // sanity
      for (Map.Entry<String, String[]> entry : denormPropMap.entrySet())
      {
         for (String s : entry.getValue())
         {
            if (s == null)
            {
               String msg = "DMO class %s is missing a denormalized field for property %s";
               throw new NullPointerException(
                  String.format(msg, targetInterface.getName(), entry.getKey()));
            }
         }
      }
      
      // analyze all methods declared specifically in the DMO interface and in the hierachy
      ArrayList<Method> allMethods = new ArrayList<>(Arrays.asList(dmoIface.getDeclaredMethods()));
      for (Class<?> parent : dmoIface.getInterfaces())
      {
         allMethods.addAll(Arrays.asList(parent.getDeclaredMethods()));
      }

      for (Method method : allMethods)
      {
         String property = getPropertyName(method);
         if (property == null)
         {
            System.out.println("Unknown property for method " + method); // TODO: LOG it?
            continue;
         }
         
         if (normPropExtentMap.containsKey(property) || denormPropMap.containsKey(property))
         {
            analyzeExtentPropertyMethod(method, property);
         }
         else if (legacyProps.contains(property) || reverseDenormPropMap.containsKey(property))
         {
            analyzeSimplePropertyMethod(method, property);
         }
         else
         {
            System.out.println("Unknown type of property " + property + " of method " + method); // TODO: LOG it?
         }
      }
      
      // store all method PropertyHelper property mappings related to this buffer interface's
      // enclosing class; we use the enclosing interface of the actual DMO buffer interface
      // passed to the constructor, rather than the target buffer interface's enclosing interface,
      // because they might be different for temp-table DMOs, and we have to ensure that any
      // subsequent lookups for the buffer associated with this proxy don't fail
      PropertyHelper.putExtentsByProperty(dmoIface, extentMap);
      PropertyHelper.putLegacyGettersByProperty(dmoIface, legacyGetterMap);
      PropertyHelper.putLegacySettersByProperty(dmoIface, legacySetterMap);
      PropertyHelper.putLegacyBulkGettersByProperty(dmoIface, arrayBulkGetterMap);
      PropertyHelper.putLegacyBulkSettersByProperty(dmoIface, arrayBulkSetterMap);
//      scalarBulkSetterMap ??
      
      // get maps of all the POJO getters and setters for the enclosing interface
      pojoGetterMap = PropertyHelper.pojoGettersByProperty(dmoIface);
      pojoSetterMap = PropertyHelper.pojoSettersByProperty(dmoIface);
      
      // there is an additional classification of methods which will not be declared in the inner
      // interface, but rather in the enclosing DMO interface: normalized, indexed getters and
      // setters; we need to proxy these as well, in order to implement index range checking code
      if (!normPropExtentMap.isEmpty())
      {
         for (Map.Entry<String, Integer> entry : normPropExtentMap.entrySet())
         {
            String property = entry.getKey();
            Integer extent = entry.getValue();
            
            if (methodAssemblerMap == null)
            {
               methodAssemblerMap = new HashMap<>();
            }
            
            Method method = legacyGetterMap.get(property);
            if (method == null)
            {
               String msg = "No getter method found in DMO interface %s for property %s";
               throw new NullPointerException(String.format(msg, dmoIface.getName(), property));
            }
            methodAssemblerMap.put(method, new IndexedGetterAssembler(extent));
            
            method = legacySetterMap.get(property);
            if (method == null)
            {
               String msg = "No setter method found in DMO interface %s for property %s";
               throw new NullPointerException(String.format(msg, dmoIface.getName(), property));
            }
            methodAssemblerMap.put(method, new IndexedSetterAssembler(extent));
         }
      }
   }
   
   /**
    * Analyze the instance fields of a DMO interface, or inner composite class, for
    * the purpose of categorizing the fields, extracting annotation metadata, and later
    * cross-referencing this information with DMO interface methods which require proxy
    * implementations.
    * 
    * @param   cls
    *          DMO implementation class or inner composite class which contains instance fields.
    */
   private void analyzeFields(Class<? extends DataModelObject> cls)
   {
      DmoMeta dmoInfo;
      Iterator<Property> it;
      
      try 
      {
         dmoInfo = DmoMetadataManager.getDmoInfo(cls);
         it = dmoInfo.getFields(true);
      }
      catch (IllegalArgumentException e) 
      {
         if (catchDmoMetaException)
         {
            it = DmoMeta.getPropertiesList(cls, Temporary.class.isAssignableFrom(cls)).iterator();
         }
         else 
         {
            throw e;
         }
      }
      
      it.forEachRemaining(property -> 
      {
         String propertyName = property.name;
         
         int extent = property.extent;
         if (extent == 0)
         {
            // legacy scalar field
            legacyProps.add(propertyName);
         }
         else
         {
            int index = property.index;
            if (index == 0)
            {
               // if index is 0, we are in an inner, composite class, visiting the (java)
               // instance field which represents a normalized (4GL) extent field
               normPropExtentMap.put(propertyName, extent);
               extentMap.put(propertyName, extent);
               
               // legacy extent field
               legacyProps.add(propertyName);
            }
            else
            {
               // otherwise, we are visiting the (java) instance field in the top-level DMO
               // class which represents a denormalized (4GL) extent field
               
               // property name which would be have been used for normalized field
               String baseProperty = property.original;
               extentMap.put(baseProperty, extent);
               
               // try to look up denormalized property info by base property name, create and map
               // the array if it doesn't exist
               String[] denormProps = denormPropMap.computeIfAbsent(baseProperty, k -> new String[extent]);
               
               // set denormalized property at appropriate index in extent data list
               denormProps[index - 1] = propertyName;
               
               // update the reversed lookup map
               reverseDenormPropMap.put(propertyName, Pair.of(baseProperty, index));
            }
         }
      });
   }
   
   /**
    * Analyze a method declared by the root DMO buffer interface, and categorize this method for
    * further processing/assembly.
    * <p>
    * The <code>method</code> argument must conform to one of the following signatures, where
    * "foo" is an example property name (note: access modifiers intentionally are omitted, and
    * the generics here are intended only to express valid types; however, the actual method
    * signature must not include generics):
    * <pre>
    *    T&lt;? extends BaseDataType&gt;   getFoo(int index)
    *    T&lt;? extends BaseDataType&gt;   getFoo(NumberType index)
    *    T&lt;? extends BaseDataType&gt;[] getFoo()
    *                         void   setFoo(int index, T&lt;? extendsBaseDataType&gt; foo)
    *                         void   setFoo(NumberType index, T&lt;? extendsBaseDataType&gt; foo)
    *                         void   setFoo(T&lt;? extendsBaseDataType&gt; foo)
    *                         void   setFoo(T&lt;? extendsBaseDataType&gt;[] foo)
    * </pre>
    * <p>
    * If any other signature is encountered, a warning is logged and this method returns
    * immediately.
    * 
    * @param   method
    *          Method requiring analysis.
    * @param   property
    *          The property accessed by the method. Must not be {@code null}.
    */
   private void analyzeExtentPropertyMethod(Method method, String property)
   {
      method.setAccessible(true);
      Class<?> returnType = method.getReturnType();
      Class<?>[] parmTypes = method.getParameterTypes();
      int parms = parmTypes.length;
      boolean isGetter = !returnType.equals(Void.TYPE);
      
      if (isGetter) // try to match indexed getter method
      {
         if (parms == 1 && BaseDataType.class.isAssignableFrom(returnType))
         {
            Class<?> parmType = parmTypes[0];
            if (Integer.TYPE.equals(parmType))
            {
               if (!denormPropMap.containsKey(property))
               {
                  primGetterMap.put(property, method);
               }
               legacyGetterMap.put(property, method);
               return;
            }
            else if (NumberType.class.equals(parmType))
            {
               bdtGetterMap.put(property, method);
               methods.add(method);
               return;
            }
         }
         else if (returnType.isArray() && parms == 0) // try to match array getter method
         {
            Class<?> compType = returnType.getComponentType();
            if (BaseDataType.class.isAssignableFrom(compType))
            {
               arrayBulkGetterMap.put(property, method);
               methods.add(method);
               return;
            }
         }
      }
      else
      {
         // try to match indexed setter method
         if (parms == 2 && BaseDataType.class.isAssignableFrom(parmTypes[1]))
         {
            Class<?> indexType = parmTypes[0];
            if (Integer.TYPE.equals(indexType))
            {
               if (!denormPropMap.containsKey(property))
               {
                  primSetterMap.put(property, method);
               }
               legacySetterMap.put(property, method);
               return;
            }
            else if (NumberType.class.equals(indexType))
            {
               bdtSetterMap.put(property, method);
               methods.add(method);
               return;
            }
         }
         else if (parms == 1) // try to match a bulk setter method
         {
            if (parmTypes[0].isArray()) // try to match array bulk setter method
            {
               Class<?> compType = parmTypes[0].getComponentType();
               if (BaseDataType.class.isAssignableFrom(compType))
               {
                  arrayBulkSetterMap.put(property, method);
                  methods.add(method);
                  return;
               }
            }
            else if (BaseDataType.class.isAssignableFrom(parmTypes[0])) // try to match scalar bulk setter method
            {
               scalarBulkSetterMap.put(property, method);
               methods.add(method);
               return;
            }
         }
      }
      
      // all expected methods have been matched by now
      logUnexpectedMethod(method);
   }
   
   /**
    * Analyze a method declared by the root DMO buffer interface's enclosing interface, and
    * categorize this method for further processing/assembly.
    * <p>
    * The <code>method</code> argument must conform to one of the following signatures, where
    * "foo" is an example property name (note: access modifiers intentionally are omitted, and
    * the generics here are intended only to express valid types; however, the actual method
    * signature must not include generics):
    * <pre>
    *    T&lt;? extends BaseDataType&gt; getFoo()
    *    T&lt;? extends BaseDataType&gt; getFoo(int index)
    *                         void setFoo(T&lt;? extendsBaseDataType&gt; foo)
    *                         void setFoo(int index, T&lt;? extendsBaseDataType&gt; foo)
    * </pre>
    * <p>
    * If any other signature is encountered, a warning is logged and this method returns
    * immediately.
    * 
    * @param   method
    *          Method requiring analysis.
    * @param   property
    *          The property accessed by the method. Must not be {@code null}.
    */
   private void analyzeSimplePropertyMethod(Method method, String property)
   {
      Class<?> returnType = method.getReturnType();
      boolean isGetter = !returnType.equals(Void.TYPE);
      Class<?>[] parmTypes = method.getParameterTypes();
      int parms = parmTypes.length;
      
      if (normPropExtentMap.containsKey(property))
      {
         if (isGetter)
         {
            if (parms == 1 && Integer.TYPE.equals(parmTypes[0]))
            {
               legacyGetterMap.put(property, method);
               return;
            }
         }
         else
         {
            if (parms == 2 && Integer.TYPE.equals(parmTypes[0]))
            {
               legacySetterMap.put(property, method);
               return;
            }
         }
      }
      else if (isGetter)
      {
         if (parms == 0 && BaseDataType.class.isAssignableFrom(returnType))
         {
            legacyGetterMap.put(property, method);
            return;
         }
      }
      else if (parms == 1 && BaseDataType.class.isAssignableFrom(parmTypes[0]))
      {
         legacySetterMap.put(property, method);
         return;
      }
      
      logUnexpectedMethod(method);
   }
   
   /**
    * Extract the DMO property name from the given method, which is assumed to be a getter or
    * setter method.
    * <p>
    * If the method does not follow the naming convention of a getter or setter method, log a
    * warning and return <code>null</code>.
    * 
    * @param   method
    *          DMO getter/setter method.
    * 
    * @return  Property name, or <code>null</code> if method is not a getter or setter.
    */
   private String getPropertyName(Method method)
   {
      try
      {
         return PropertyHelper.getPropertyName(method).intern();
      }
      catch (IllegalArgumentException exc)
      {
         // not a getter or setter
         logUnexpectedMethod(method);
      }
      
      return null;
   }
   
   /**
    * Assemble an indexed getter method which accepts a primitive <code>int</code> index and
    * delegates to the appropriate, denormalized, simple getter method.
    * <p>
    * The source equivalent for an <code>integer</code> property <code>foo</code> with extent
    * <code>N</code> and default denormalization naming would be:
    * <pre>
    * public integer getFoo(int index)
    * {
    *    switch (index)
    *    {
    *       case 0:
    *          return getFoo1();
    *       case 1:
    *          return getFoo2();
    *       ...
    *       case N:
    *          return getFooN();
    *    }
    *    
    *    outOfBoundsError(index);
    *    integer unk = new integer();
    *    unk.setUnknown();
    *    
    *    return unk;
    * }
    * </pre>
    * 
    * @param   classWriter
    *          ASM class writer
    * @param   dmoBufIfaceType
    *          Internal type name of the interface containing the method being implemented and
    *          the methods it calls.
    * @param   baseProperty
    *          Base property name associated with the indexed setter.
    * @param   method
    *          Indexed setter method.
    */
   private void assembleDenormPrimGetter(ClassWriter classWriter,
                                         String dmoBufIfaceType,
                                         String baseProperty,
                                         Method method)
   {
      // look up the denormalized property names associated with this base property
      String[] denormProps = denormPropMap.get(baseProperty);
      int len = denormProps == null ? 0 : denormProps.length;
      if (len == 0)
      {
         return;
      }
      
      // create method visitor
      String baseDesc = ProxyUtils.makeMethodDescriptor(method);
      String methName = method.getName();
      MethodVisitor mv =
         classWriter.visitMethod(ACC_PUBLIC,
                                 methName,
                                 baseDesc,
                                 null,
                                 ProxyUtils.getCaughtExceptionTypeNames(method));
      
      // compose array of possible case values
      int[] cases = new int[len];
      for (int i = 0; i < len; i++)
      {
         cases[i] = i;
      }
      
      // compose array labels for lookupswitch instruction
      Label[] switchLabels = new Label[len];
      for (int i = 0; i < len; i++)
      {
         switchLabels[i] = new Label();
      }
      
      // label for lookupswitch instruction's default jump
      Label defLabel = new Label();
      
      // start bytecode
      mv.visitCode();
      
      // load index parameter
      mv.visitVarInsn(ILOAD, 1);
      
      // switch statement
      mv.visitLookupSwitchInsn(defLabel, cases, switchLabels);
      
      // switch cases
      for (int i = 0; i < len; i++)
      {
         // get denormalized method to invoke
         String property = denormProps[i];
         Method denormMeth = pojoGetterMap.get(property);
         String denormName = denormMeth.getName();
         String denormDesc = ProxyUtils.makeMethodDescriptor(denormMeth);
         
         // mark this as the jump location for case i
         mv.visitLabel(switchLabels[i]);
         
         // load this object reference
         mv.visitVarInsn(ALOAD, 0);
         
         // invoke the denormalized, simple, getter method
         mv.visitMethodInsn(INVOKEINTERFACE, dmoBufIfaceType, denormName, denormDesc, true);
         
         // return object reference from stack
         mv.visitInsn(ARETURN);
      }
      
      // default case
      mv.visitLabel(defLabel);
      
      // load this object reference
      mv.visitVarInsn(ALOAD, 0);
      
      // load index parameter
      mv.visitVarInsn(ILOAD, 1);
      
      // invoke BufferImpl.outOfBoundsError method
      mv.visitMethodInsn(INVOKEVIRTUAL, TYPE_BUFFERIMPL, METH_OOBE, SIG_OOBE, false);
      
      // instantiate and return a wrapper of the appropriate type, set to unknown value
      String typeName = AsmUtils.commonToInternalTypeName(method.getReturnType());
      assembleReturnUnknown(mv, typeName, 2);
      
      // parameters ignored
      mv.visitMaxs(0, 0);
      
      // end bytecode for this method
      mv.visitEnd();
   }
   
   /**
    * Assemble an indexed setter method which accepts a primitive <code>int</code> index and
    * delegates to the appropriate, denormalized, simple setter method.
    * <p>
    * The source equivalent for an <code>integer</code> property <code>foo</code> with extent
    * <code>N</code> and default denormalization naming would be:
    * <pre>
    * public void setFoo(int index, integer foo)
    * {
    *    switch (index)
    *    {
    *       case 0:
    *          setFoo1(foo);
    *          return;
    *       case 1:
    *          setFoo2(foo);
    *          return;
    *       ...
    *       case N:
    *          setFooN();
    *          return;
    *    }
    *    
    *    outOfBoundsError(index);
    * }
    * </pre>
    * 
    * @param   classWriter
    *          ASM class writer.
    * @param   dmoBufIfaceType
    *          Internal type name of the interface containing the method being implemented and
    *          the methods it calls.
    * @param   baseProperty
    *          Base property name associated with the indexed setter.
    * @param   method
    *          Indexed setter method.
    */
   private void assembleDenormPrimSetter(ClassWriter classWriter,
                                         String dmoBufIfaceType,
                                         String baseProperty,
                                         Method method)
   {
      // look up the denormalized property names associated with this base property
      String[] denormProps = denormPropMap.get(baseProperty);
      int len = denormProps == null ? 0 : denormProps.length;
      if (len == 0)
      {
         return;
      }
      
      // create method visitor
      String baseDesc = ProxyUtils.makeMethodDescriptor(method);
      String methName = method.getName();
      MethodVisitor mv =
         classWriter.visitMethod(ACC_PUBLIC,
                                 methName,
                                 baseDesc,
                                 null,
                                 ProxyUtils.getCaughtExceptionTypeNames(method));
      
      // compose array of possible case values
      int[] cases = new int[len];
      for (int i = 0; i < len; i++)
      {
         cases[i] = i;
      }
      
      // compose array labels for lookupswitch instruction
      Label[] switchLabels = new Label[len];
      for (int i = 0; i < len; i++)
      {
         switchLabels[i] = new Label();
      }
      
      // label for lookupswitch instruction's default jump
      Label defLabel = new Label();
      
      // start bytecode
      mv.visitCode();
      
      // load index (int) parameter
      mv.visitVarInsn(ILOAD, 1);
      
      // switch statement
      mv.visitLookupSwitchInsn(defLabel, cases, switchLabels);
      
      // switch cases
      for (int i = 0; i < len; i++)
      {
         // get denormalized method to invoke
         String property = denormProps[i];
         Method denormMeth = pojoSetterMap.get(property);
         String denormName = denormMeth.getName();
         String denormDesc = ProxyUtils.makeMethodDescriptor(denormMeth);
         
         // mark this as the jump location for case i
         mv.visitLabel(switchLabels[i]);
         
         // load this object reference
         mv.visitVarInsn(ALOAD, 0);
         
         // load the second method parameter (BDT subclass)
         mv.visitVarInsn(ALOAD, 2);
         
         // invoke the denormalized, simple, getter method
         mv.visitMethodInsn(INVOKEINTERFACE, dmoBufIfaceType, denormName, denormDesc, true);
         
         // return void
         mv.visitInsn(RETURN);
      }
      
      // default case
      mv.visitLabel(defLabel);
      
      // load this object reference
      mv.visitVarInsn(ALOAD, 0);
      
      // load index parameter
      mv.visitVarInsn(ILOAD, 1);
      
      // invoke BufferImpl.outOfBoundsError method
      mv.visitMethodInsn(INVOKEVIRTUAL, TYPE_BUFFERIMPL, METH_OOBE, SIG_OOBE, false);
      
      // return void
      mv.visitInsn(RETURN);
      
      // parameters ignored
      mv.visitMaxs(0, 0);
      
      // end bytecode for this method
      mv.visitEnd();
   }
   
   /**
    * Assemble an indexed getter method which accepts a <code>NumberType</code> index, does an
    * unknown value check, and delegates to the corresponding, indexed getter method.
    * <p>
    * The source equivalent for an <code>integer</code> property <code>foo</code> with default
    * denormalization naming would be:
    * <pre>
    * public integer getFoo(NumberType index)
    * {
    *    if (index.isUnknown())
    *    {
    *       integer unk = new integer();
    *       unk.setUnknown();
    *       
    *       return unk;
    *    }
    *    
    *    return getFoo(index.intValue());
    * }
    * </pre>
    * 
    * @param   classWriter
    *          ASM class writer
    * @param   dmoBufIfaceType
    *          Internal type name of the interface containing the method being implemented and
    *          the methods it calls.
    * @param   baseProperty
    *          Base property name associated with the indexed getter.
    * @param   method
    *          Indexed getter method.
    */
   private void assembleDenormBdtGetter(ClassWriter classWriter,
                                        String dmoBufIfaceType,
                                        String baseProperty,
                                        Method method)
   {
      // create method visitor
      String methDesc = ProxyUtils.makeMethodDescriptor(method);
      String methName = method.getName();
      MethodVisitor mv =
         classWriter.visitMethod(ACC_PUBLIC,
                                 methName,
                                 methDesc,
                                 null,
                                 ProxyUtils.getCaughtExceptionTypeNames(method));
      
      // start bytecode
      mv.visitCode();
      
      // check index parameter for unknown value and jump past conditional block if test fails
      Label jumpLabel = assembleUnknownValueCheck(mv);
      
      // instantiate and return a wrapper of the appropriate type, set to unknown value
      String typeName = AsmUtils.commonToInternalTypeName(method.getReturnType());
      assembleReturnUnknown(mv, typeName, 2);
      
      // mark the jump location after the unknown check block
      mv.visitLabel(jumpLabel);
      
      // load this object reference
      mv.visitVarInsn(ALOAD, 0);
      
      // load index parameter
      mv.visitVarInsn(ALOAD, 1);
      
      // extract its primitive int value
      mv.visitMethodInsn(INVOKEVIRTUAL, TYPE_NUMBERTYPE, METH_INTVALUE, SIG_INTVALUE, false);
      
      // delegate to the indexed getter which takes a primitive int index
      Method primGetter = legacyGetterMap.get(baseProperty);
      String primMethDesc = ProxyUtils.makeMethodDescriptor(primGetter);
      mv.visitMethodInsn(INVOKEINTERFACE, dmoBufIfaceType, methName, primMethDesc, true);
      
      // return the retrieved value
      mv.visitInsn(ARETURN);
      
      // parameters ignored
      mv.visitMaxs(0, 0);
      
      // end bytecode for this method
      mv.visitEnd();
   }
   
   /**
    * Assemble an indexed setter method which accepts a <code>NumberType</code> index, does an
    * unknown value check, and delegates to the corresponding, indexed getter method.
    * <p>
    * The source equivalent for an <code>integer</code> property <code>foo</code> with default
    * denormalization naming would be:
    * <pre>
    * public void setFoo(NumberType index, integer foo)
    * {
    *    if (index.isUnknown())
    *    {
    *       return;
    *    }
    *    
    *    setFoo(index.intValue(), foo);
    * }
    * </pre>
    * 
    * @param   classWriter
    *          ASM class writer
    * @param   dmoBufIfaceType
    *          Internal type name of the interface containing the method being implemented and
    *          the methods it calls.
    * @param   baseProperty
    *          Base property name associated with the indexed setter.
    * @param   method
    *          Indexed setter method.
    */
   private void assembleDenormBdtSetter(ClassWriter classWriter,
                                        String dmoBufIfaceType,
                                        String baseProperty,
                                        Method method)
   {
      // create method visitor
      String methDesc = ProxyUtils.makeMethodDescriptor(method);
      String methName = method.getName();
      MethodVisitor mv =
         classWriter.visitMethod(ACC_PUBLIC,
                                 methName,
                                 methDesc,
                                 null,
                                 ProxyUtils.getCaughtExceptionTypeNames(method));
      
      // start bytecode
      mv.visitCode();
      
      // check index parameter for unknown value and jump past conditional block if test fails
      Label jumpLabel = assembleUnknownValueCheck(mv);
      
      // return immediately if index was unknown value
      mv.visitInsn(RETURN);
      
      // mark the jump location after the unknown check block
      mv.visitLabel(jumpLabel);
      
      // load this object reference
      mv.visitVarInsn(ALOAD, 0);
      
      // load index parameter
      mv.visitVarInsn(ALOAD, 1);
      
      // extract its primitive int value
      mv.visitMethodInsn(INVOKEVIRTUAL, TYPE_NUMBERTYPE, METH_INTVALUE, SIG_INTVALUE, false);
      
      // load the second method parameter (BDT subclass)
      mv.visitVarInsn(ALOAD, 2);
      
      // delegate to the indexed setter which takes a primitive int index
      Method primSetter = legacySetterMap.get(baseProperty);
      String primMethDesc = ProxyUtils.makeMethodDescriptor(primSetter);
      mv.visitMethodInsn(INVOKEINTERFACE, dmoBufIfaceType, methName, primMethDesc, true);
      
      // return void
      mv.visitInsn(RETURN);
      
      // parameters ignored
      mv.visitMaxs(0, 0);
      
      // end bytecode for this method
      mv.visitEnd();
   }
   
   /**
    * Assemble a getter method which returns an array containing all the values in an array
    * property by delegating to the corresponding, primitive, indexed getter method in a loop.
    * <p>
    * The source equivalent for an <code>integer</code> property <code>foo</code> with extent
    * <code>N</code> and default denormalization naming would be:
    * <pre>
    * public integer[] getFoo()
    * {
    *    integer[] fooArray = new integer[N];
    *    for (int i = 0; i &lt; N; i++)
    *    {
    *       fooArray[i] = getFoo(i);
    *    }
    *    return fooArray;
    * }
    * </pre>
    * 
    * @param   classWriter
    *          ASM class writer
    * @param   dmoBufIfaceType
    *          Internal type name of the interface containing the method being implemented and
    *          the methods it calls.
    * @param   baseProperty
    *          Base property name associated with the getter.
    * @param   method
    *          Array bulk getter method.
    */
   private void assembleArrayBulkGetter(ClassWriter classWriter,
                                        String dmoBufIfaceType,
                                        String baseProperty,
                                        Method method)
   {
      // create method visitor
      String methDesc = ProxyUtils.makeMethodDescriptor(method);
      String methName = method.getName();
      MethodVisitor mv =
         classWriter.visitMethod(ACC_PUBLIC,
                                 methName,
                                 methDesc,
                                 null,
                                 ProxyUtils.getCaughtExceptionTypeNames(method));
      
      // start bytecode
      mv.visitCode();
      
      // push the extent size onto the stack
      int extent = extentMap.get(baseProperty);
      AsmUtils.pushInt(mv, extent);
      
      // instantiate an array in which to store results
      Class<?> type = method.getReturnType().getComponentType();
      String typeName = AsmUtils.commonToInternalTypeName(type);
      mv.visitTypeInsn(ANEWARRAY, typeName);
      
      // store the array in a local variable
      mv.visitVarInsn(ASTORE, 1);
      
      // push starting loop counter value onto stack
      mv.visitInsn(ICONST_0);
      
      // store current value of loop counter in a local variable
      mv.visitVarInsn(ISTORE, 2);
      
      // mark top of loop
      Label topOfLoop = new Label();
      mv.visitLabel(topOfLoop);
      
      // load loop counter from local variable
      mv.visitVarInsn(ILOAD, 2);
      
      // push loop limit value onto stack for comparison with counter
      AsmUtils.pushInt(mv, extent);
      
      // jump past loop body if counter >= limit
      Label afterLoop = new Label();
      mv.visitJumpInsn(IF_ICMPGE, afterLoop);
      
      // load the array
      mv.visitVarInsn(ALOAD, 1);
      
      // load the loop counter to use as the index into the array
      mv.visitVarInsn(ILOAD, 2);
      
      // load this object reference
      mv.visitVarInsn(ALOAD, 0);
      
      // load the loop counter to use as parameter for indexed getter method
      mv.visitVarInsn(ILOAD, 2);
      
      // invoke the indexed getter method
      Method primGetter = legacyGetterMap.get(baseProperty);
      String primMethDesc = ProxyUtils.makeMethodDescriptor(primGetter);
      mv.visitMethodInsn(INVOKEINTERFACE, dmoBufIfaceType, methName, primMethDesc, true);
      
      // store the returned value in the array
      mv.visitInsn(AASTORE);
      
      // increment loop counter (local variable 2) by 1
      mv.visitIincInsn(2, 1);
      
      // jump back to top of loop
      mv.visitJumpInsn(GOTO, topOfLoop);
      
      // mark afterLoop jump location
      mv.visitLabel(afterLoop);
      
      // load the array
      mv.visitVarInsn(ALOAD, 1);
      
      // return the array
      mv.visitInsn(ARETURN);
      
      // parameters ignored
      mv.visitMaxs(0, 0);
      
      // end of bytecode for this method
      mv.visitEnd();
   }
   
   /**
    * Assemble a setter method which accepts an array containing zero or more values to be stored
    * in an array property, at the corresponding element positions, by delegating to the
    * corresponding, primitive, indexed setter method in a loop.
    * <p>
    * The source equivalent for an <code>integer</code> property <code>foo</code> with extent
    * <code>N</code> and default denormalization naming would be:
    * <pre>
    * public void setFoo(integer[] foo)
    * {
    *    int len = Math.min(foo.length, N);
    *    for (int i = 0; i &lt; len; i++)
    *    {
    *       setFoo(i, foo[i]);
    *    }
    * }
    * </pre>
    * 
    * @param   classWriter
    *          ASM class writer
    * @param   dmoBufIfaceType
    *          Internal type name of the interface containing the method being implemented and
    *          the methods it calls.
    * @param   baseProperty
    *          Base property name associated with the setter.
    * @param   method
    *          Array bulk setter method.
    */
   private void assembleArrayBulkSetter(ClassWriter classWriter,
                                        String dmoBufIfaceType,
                                        String baseProperty,
                                        Method method)
   {
      // create method visitor
      String methDesc = ProxyUtils.makeMethodDescriptor(method);
      String methName = method.getName();
      MethodVisitor mv =
         classWriter.visitMethod(ACC_PUBLIC,
                                 methName,
                                 methDesc,
                                 null,
                                 ProxyUtils.getCaughtExceptionTypeNames(method));
      
      // start bytecode
      mv.visitCode();
      
      // load array parameter
      mv.visitVarInsn(ALOAD, 1);
      
      // push array length onto stack
      mv.visitInsn(ARRAYLENGTH);
      
      // push the extent size onto the stack
      int extent = extentMap.get(baseProperty);
      AsmUtils.pushInt(mv, extent);
      
      // get the minimum of the array length and the extent size
      mv.visitMethodInsn(INVOKESTATIC, TYPE_MATH, METH_MIN, SIG_MIN, false);
      
      // store resulting min value (our loop limit) in a local variable
      mv.visitVarInsn(ISTORE, 2);
      
      // push starting loop counter value onto stack
      mv.visitInsn(ICONST_0);
      
      // store current value of loop counter in a local variable
      mv.visitVarInsn(ISTORE, 3);
      
      // mark top of loop
      Label topOfLoop = new Label();
      mv.visitLabel(topOfLoop);
      
      // load loop counter
      mv.visitVarInsn(ILOAD, 3);
      
      // load loop limit
      mv.visitVarInsn(ILOAD, 2);
      
      // jump past loop body in counter >= limit
      Label afterLoop = new Label();
      mv.visitJumpInsn(IF_ICMPGE, afterLoop);
      
      // load this object reference
      mv.visitVarInsn(ALOAD, 0);
      
      // load loop counter to use as parameter for indexed setter method
      mv.visitVarInsn(ILOAD, 3);
      
      // load array reference
      mv.visitVarInsn(ALOAD, 1);
      
      // load the loop counter to use as the index into the array
      mv.visitVarInsn(ILOAD, 3);
      
      // load array value at that index
      mv.visitInsn(AALOAD);
      
      // invoke the indexed setter method
      Method primSetter = legacySetterMap.get(baseProperty);
      String primMethDesc = ProxyUtils.makeMethodDescriptor(primSetter);
      mv.visitMethodInsn(INVOKEINTERFACE, dmoBufIfaceType, methName, primMethDesc, true);
      
      // increment loop counter (local variable 3) by 1
      mv.visitIincInsn(3, 1);
      
      // jump back to top of loop
      mv.visitJumpInsn(GOTO, topOfLoop);
      
      // mark afterLoop jump location
      mv.visitLabel(afterLoop);
      
      // return void
      mv.visitInsn(RETURN);
      
      // parameters ignored
      mv.visitMaxs(0, 0);
      
      // end of bytecode for this method
      mv.visitEnd();
   }
   
   /**
    * Assemble a setter method which accepts a scalar value and store that value into each
    * element in an array property, by delegating to the corresponding, primitive, indexed setter
    * method in a loop.
    * <p>
    * The source equivalent for an <code>integer</code> property <code>foo</code> with extent
    * <code>N</code> and default denormalization naming would be:
    * <pre>
    * public void setFoo(integer foo)
    * {
    *    for (int i = 0; i &lt; N; i++)
    *    {
    *       setFoo(i, foo);
    *    }
    * }
    * </pre>
    * 
    * @param   classWriter
    *          ASM class writer
    * @param   dmoBufIfaceType
    *          Internal type name of the interface containing the method being implemented and
    *          the methods it calls.
    * @param   baseProperty
    *          Base property name associated with the setter.
    * @param   method
    *          Scalar bulk setter method.
    */
   private void assembleScalarBulkSetter(ClassWriter classWriter,
                                         String dmoBufIfaceType,
                                         String baseProperty,
                                         Method method)
   {
      // create method visitor
      String methDesc = ProxyUtils.makeMethodDescriptor(method);
      String methName = method.getName();
      MethodVisitor mv =
         classWriter.visitMethod(ACC_PUBLIC,
                                 methName,
                                 methDesc,
                                 null,
                                 ProxyUtils.getCaughtExceptionTypeNames(method));
      
      // start bytecode
      mv.visitCode();
      
      // push the extent size onto the stack
      int extent = extentMap.get(baseProperty);
      AsmUtils.pushInt(mv, extent);
      
      // store extent size (our loop limit) in a local variable
      mv.visitVarInsn(ISTORE, 2);
      
      // push starting loop counter value onto stack
      mv.visitInsn(ICONST_0);
      
      // store current value of loop counter in a local variable
      mv.visitVarInsn(ISTORE, 3);
      
      // mark top of loop
      Label topOfLoop = new Label();
      mv.visitLabel(topOfLoop);
      
      // load loop counter
      mv.visitVarInsn(ILOAD, 3);
      
      // load loop limit
      mv.visitVarInsn(ILOAD, 2);
      
      // jump past loop body in counter >= limit
      Label afterLoop = new Label();
      mv.visitJumpInsn(IF_ICMPGE, afterLoop);
      
      // load this object reference
      mv.visitVarInsn(ALOAD, 0);
      
      // load loop counter to use as parameter for indexed setter method
      mv.visitVarInsn(ILOAD, 3);
      
      // load the method parameter (BDT subclass)
      mv.visitVarInsn(ALOAD, 1);
      
      // invoke the indexed setter method
      Method primSetter = legacySetterMap.get(baseProperty);
      String primMethDesc = ProxyUtils.makeMethodDescriptor(primSetter);
      mv.visitMethodInsn(INVOKEINTERFACE, dmoBufIfaceType, methName, primMethDesc, true);
      
      // increment loop counter (local variable 3) by 1
      mv.visitIincInsn(3, 1);
      
      // jump back to top of loop
      mv.visitJumpInsn(GOTO, topOfLoop);
      
      // mark afterLoop jump location
      mv.visitLabel(afterLoop);
      
      // return void
      mv.visitInsn(RETURN);
      
      // parameters ignored
      mv.visitMaxs(0, 0);
      
      // end of bytecode for this method
      mv.visitEnd();
   }
   
   /**
    * Assemble the instructions to test whether the <code>NumberType</code> object in local
    * variable slot 1 (i.e., the subscript parameter to an indexed getter/setter method)
    * represents the unknown value, and to jump to the instruction represented by the returned
    * label if so. It is up to the caller to assign the label to a jump destination.
    * 
    * @param   mv
    *          Method visitor.
    * 
    * @return  Jump label, to allow the caller to determine the jump destination, in the event of
    *          a failed check (i.e., unknown == true).
    */
   private Label assembleUnknownValueCheck(MethodVisitor mv)
   {
      // load index (NumberType) parameter
      mv.visitVarInsn(ALOAD, 1);
      
      // check whether index is unknown
      mv.visitMethodInsn(INVOKEVIRTUAL, TYPE_NUMBERTYPE, METH_ISUNK, SIG_ISUNK, false);
      
      // jump past block if index was not unknown
      Label jumpLabel = new Label();
      mv.visitJumpInsn(IFEQ, jumpLabel);
      
      return jumpLabel;
   }
   
   /**
    * Log a warning that an unexpected method was encountered while introspecting a DMO
    * interface or DMO buffer interface.
    * 
    * @param   method
    *          Unexpected method.
    */
   private void logUnexpectedMethod(Method method)
   {
      if (LOG.isLoggable(Level.WARNING))
      {
         String msg = "Unexpected method in DMO buffer target interface %s: %s";
         LOG.warning(String.format(msg, targetInterface.getName(), method.toString()));
      }
   }
   
   /**
    * A base class for concrete implementations of indexed proxy method assemblers.
    */
   protected abstract static class IndexedProxyMethod
   extends ProxyAssembler.ProxyMethod
   {
      /** Extent of the associated property */
      private final int extent;
      
      /**
       * Default constructor.
       * 
       * @param   extent
       *          Extent of the associated property.
       */
      IndexedProxyMethod(int extent)
      {
         super();
         
         this.extent = extent;
      }
      
      /**
       * Assemble the bytecode instructions to perform a range check on a primitive integer value
       * in local variable 1 (the subscript parameter of an indexed getter/setter method), and to
       * jump to the instruction represented by the returned label if the check passes. If the
       * check fails, the instruction next visited by the caller is executed. It is up to the
       * caller to assign the label to a jump destination.
       * <p>
       * The assembled method will delegate to the {@link BufferImpl#checkExtentRange(int, int)
       * parent class' range check method} for the range check itself.
       * 
       * @param   mv
       *          Method visitor
       * 
       * @return  Jump label, to allow the caller to determine the jump destination, in the event
       *          of a failed check (i.e., subscript is out of range).
       */
      protected Label assembleRangeCheck(MethodVisitor mv)
      {
         // load this object reference
         mv.visitVarInsn(ALOAD, 0);
         
         // load the first method parameter (index)
         mv.visitVarInsn(ILOAD, 1);
         
         // push the extent size onto the stack
         AsmUtils.pushInt(mv, extent);
         
         // invoke BufferImpl.checkExtentRange
         mv.visitMethodInsn(INVOKEVIRTUAL, TYPE_BUFFERIMPL, METH_CHKXRNG, SIG_CHKXRNG, false);
         
         // jump past block if index was valid
         Label jumpLabel = new Label();
         mv.visitJumpInsn(IFNE, jumpLabel);
         
         return jumpLabel;
      }
   }
   
   /**
    * This class assembles an indexed getter method which performs a range check on the index
    * before calling the proxy's invocation handler.
    */
   private static class IndexedGetterAssembler
   extends IndexedProxyMethod
   {
      /**
       * Default constructor.
       * 
       * @param   extent
       *          Extent of the associated property.
       */
      IndexedGetterAssembler(int extent)
      {
         super(extent);
      }
      
      /**
       * Assemble the bytecode which comprises the proxy method. We simply add the range check at
       * the top of the method and delegate to the superclass to implement the remainder of the
       * method.
       * 
       * @param   classWriter
       *          Class writer for proxy class.
       * @param   mv
       *          Method visitor for this method.
       * @param   method
       *          Method for which this proxy is being implemented.
       * @param   methodIndex
       *          Index of this method within the array of methods used by the invocation handler.
       * @param   parentIsHandler
       *          <code>true</code> if the parent class implements the
       *          <code>InvocationHandler</code> interface, else <code>false</code>.  If
       *          <code>true</code>, no separate instance field will be assembled to store the
       *          invocation handler reference, as the instance of this class itself will act as
       *          the handler.
       * @param   proxyName
       *          Internal form of the proxy class' name.
       */
      public void assemble(ClassWriter classWriter,
                           MethodVisitor mv,
                           Method method,
                           int methodIndex,
                           boolean parentIsHandler,
                           String proxyName)
      {
         // perform the range check
         Label jumpLabel = assembleRangeCheck(mv);
         
         // instantiate and return a wrapper of the appropriate type, set to unknown value
         String typeName = AsmUtils.commonToInternalTypeName(method.getReturnType());
         assembleReturnUnknown(mv, typeName, 2);
         
         // mark the jump location after the range check block
         mv.visitLabel(jumpLabel);
         
         // let the superclass assemble the remainder of the method
         super.assemble(classWriter, mv, method, methodIndex, parentIsHandler, proxyName);
      }
   }
   
   /**
    * This class assembles an indexed setter method which performs a range check on the index
    * before calling the proxy's invocation handler.
    */
   private static class IndexedSetterAssembler
   extends IndexedProxyMethod
   {
      /**
       * Default constructor.
       * 
       * @param   extent
       *          Extent of the associated property.
       */
      IndexedSetterAssembler(int extent)
      {
         super(extent);
      }
      
      /**
       * Assemble the bytecode which comprises the proxy method.
       * 
       * @param   classWriter
       *          Class writer for proxy class.
       * @param   mv
       *          Method visitor for this method.
       * @param   method
       *          Method for which this proxy is being implemented.
       * @param   methodIndex
       *          Index of this method within the array of methods used by the invocation handler.
       * @param   parentIsHandler
       *          <code>true</code> if the parent class implements the
       *          <code>InvocationHandler</code> interface, else <code>false</code>.  If
       *          <code>true</code>, no separate instance field will be assembled to store the
       *          invocation handler reference, as the instance of this class itself will act as
       *          the handler.
       * @param   proxyName
       *          Internal form of the proxy class' name.
       */
      public void assemble(ClassWriter classWriter,
                           MethodVisitor mv,
                           Method method,
                           int methodIndex,
                           boolean parentIsHandler,
                           String proxyName)
      {
         // perform the range check
         Label jumpLabel = assembleRangeCheck(mv);
         
         // return immediately
         mv.visitInsn(RETURN);
         
         // mark the jump location after the range check block
         mv.visitLabel(jumpLabel);
         
         // let the superclass assemble the remainder of the method
         super.assemble(classWriter, mv, method, methodIndex, parentIsHandler, proxyName);
      }
   }
}