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