HandleOps.java

/*
+** Module   : HandleOps.java
** Abstract : Defines APIs to implement handle-specific operations.
**
** Copyright (c) 2013-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- --------------------------------------Description-----------------------------------------
** 001 CA  20130112 Created initial version.
** 002 CA  20130215 Added support for DELETE WIDGET. All deletes are done at the resource, as they
**                  need to implement Deletable.
** 003 CA  20130221 getType was renamed to getResourceType.
** 004 GES 20130302 Added CAN-QUERY and CAN-SET builtin function support.
** 005 CA  20130529 Removed explicit handling of procedure deletion, now all is done in a generic
**                  approach.
** 006 CA  20130918 Resource cleanup must be done only if the resource deleted. 
** 007 EVL 20130911 Adding implementation for CAN-QUERY/CAN-SET functions.
** 008 EVL 20131009 Fix for getting the attribute getters and setters.
** 009 EVL 20131014 Add scan the static methods to CAN-QUERY/CAN-SET functions for possible
**                  ignored attributes.
** 010 CA  20140218 Error messages use the program name as it was used at the RUN call.
** 011 CA  20140223 Fixed deletion of persistent procedures via DELETE OBJECT <system-proc-handle>.
** 012 CA  20140526 handle unwrap APIs were switched to instance method calls.
** 013 CA  20150122 Added isSystemHandle API.
** 014 CA  20151222 Added logging to invalidDelete(handle).
**     OM  20160128 Added 4140 error condition for deleting DEFAULT-WINDOW.
** 015 SBI 20160608 Added CLIPBOARD handle to isSystemHandle.
** 016 CA  20180510 Added PROFILER system handle support.
** 017 CA  20181029 Added LIST-SET-ATTRS() and LIST-QUERY-ATTRS() builtin functions.
**                  Changes related to CALL's GET-ATTR-CALL-TYPE and SET-ATTR-CALL-TYPE modes.
**                  Fixed CAN-QUERY, which can check for 4GL methods, too.
** 018 CA  20181204 Added DEBUGGER handle support.
** 019 IAS 20190501 Added SECURITY-POLICY support.
**         20190506 Added AUDIT-POLICY, AUDIT CONTROL support.
** 020 CA  20190720 If a handle is registered as a OUTPUT argument, and the resource is a DATASET
**                  or TABLE, postpone the attempted delete until the copy is performed.  This 
**                  tries to emulate the BY-REFERENCE behavior in 4GL, and this solution is not 
**                  necessarily correct.
** 021 CA  20200110 Fixed delete of BY-REFERENCE DATASET parameters.
**     HC  20200301 Added LegacyAttribute.canQuery to allow to override the result of CAN-QUERY
**                  built in legacy function.
** 022 AIL 20200623 Added support for resources which can be deleted multiple times.
**     CA  20200918 Optimized the performance for CAN-SET() and CAN-QUERY() legacy functions.
**     VVT 20210115 Bugs fixed for canQuery and canSet. See the #5064 for discussion.
**     CA  20210507 Fixed a bug in executing a CALL for a static proxy resource (like SESSION).
**     CA  20210527 Added RCODE-INFO as a system handle.
**     RFB 20210713 Add the stub methods for LIST-EVENTS, listEvents. Ref #5536.
**     CA  20210917 Javadoc fixes.
**     CA  20210928 Only dynamic by-reference dataset parameters fail delete with error 12327.
**     CA  20211025 When checking if a handle supports the attribute or method passed to CAN-QUERY or CAN-SET,
**                  we need to walk the class hierarchy and keep the state of the 'ignore' flag from the 
**                  closest definition which has the annotation for this attribute/method.
**     CA  20220707 Any PSEUDO-WIDGET resource must return true in 'isSystemHandle' - this is how 4GL behaves
**                  and solves a performance problem with 'isSystemHandle'.
**     VVT 20221003 CommonHandle.getResourceType() method renamed to resourceType() to prevent conflicts
**                  with namesakes in DMO. See #6694.
**     CA  20220613 Do not allow a DELETE of a BUFFER attached to a DATASET or TEMP-TABLE handle which is an 
**                  OUTPUT parameter.
**     VVT 20220913 CommonHandle.getResourceType() method renamed to resourceType() to prevent conflicts
**                  with namesakes in DMO. See #6694.
**     CA  20230116 Avoid using handle.unwrap, handle.getReference or other BDT usage from within FWD runtime.
** 023 CA  20230302 When deleting a temp-table, the delete can be vetoed if any buffer for the temp-table is
**                  associated with an OUTPUT dataset.  Refactored to move the resource-specific code in a 
**                  'allowDelete' method.
** 024 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 025 CA  20230817 HandleOps.delete executing for a DELETE WIDGET-POOL is not considered an explicit DELETE 
**                  OBJECT (which would prevent deleting a temp-table/buffer part of a dataset).
** 026 AI  20231114 Updated delete to remove the childs of the widget.
**         20231416 Change if condition to use instanceof instead of isAssignableFrom.
** 027 CA  20231129 Added 'PseudoWidget' marker to system handle resources.
** 028 EVL 20241004 Adding SCREEN-VALUE attribute to check for possible invalid for chained widget with call
**                  invalidChainAttribute().  For widgets: WINDOW, FRAME and FIELD-GROUP we can not get this
**                  attribute with CAN-QUERY converted call.
** 029 ICP 20250129 Used logical constants to leverage caches instances.
** 030 EVL 20250318 Fix for abbreviated attribute names resolution to proper getter or setter in canQuery()
**                  method.  We have to use full name while getting annotation.
*/
/*
** 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.util;

import com.goldencode.p2j.uast.*;
import com.goldencode.p2j.ui.*;
import com.goldencode.p2j.util.logging.*;
import com.goldencode.proxy.*;

import java.util.*;
import java.util.concurrent.*;
import java.util.function.*;
import java.util.logging.*;

import java.lang.annotation.*;
import java.lang.reflect.*;

/**
 * Provides implementation for these handle-specific operations: 
 * <ul>
 *    <li>DELETE OBJECT</li>
 *    <li>DELETE PROCEDURE</li>
 *    <li>CAN-QUERY() built-in function</li>
 *    <li>CAN-SET() built-in function</li>
 * </ul>
 * 
 * Current implementation provides support only for SESSION, FILE-INFO, 
 * LAST-EVENT, ERROR-STATUS and procedure handles.
 */
public class HandleOps
{
   /** Logger */
   private static final CentralLogger LOG = CentralLogger.get(HandleOps.class.getName());
   
   /** 
    * A map holding the string-representation of all queryable attributes and methods, per each 
    * resource class.
    */
   private static final Map<Class<?>, String> QUERY_ATTRIBUTES = new ConcurrentHashMap<>();
   
   /** 
    * A map holding the string-representation of all settable attributes, per each resource class.
    */
   private static final Map<Class<?>, String> SET_ATTRIBUTES = new ConcurrentHashMap<>();
   
   /** Mapping of the 4GL attributes to their Java setter method names. */
   private static final Map<String, String> SETTERS_TO_JAVA = new ConcurrentHashMap<>();
   
   /** Mapping of the 4GL attributes to their Java getter method names. */
   private static final Map<String, String> GETTERS_TO_JAVA = new ConcurrentHashMap<>();
   
   /** Mapping of the 4GL method to their Java method names. */
   private static final Map<String, String> METHODS_TO_JAVA = new ConcurrentHashMap<>();
   
   /**
    * A map holding the set of all gettable attribute names, per each resource class.
    */
   private static final Map<String, Set<String>> LEGACY_GETTERS = new HashMap<>();

   /**
    * A map holding the set of all settable attribute names, per each resource class.
    */
   private static final Map<String, Set<String>> LEGACY_SETTERS = new HashMap<>();

   /**
    * A map holding the set of all method names, per each resource class.
    */
   private static final Map<String, Set<String>> LEGACY_METHODS = new HashMap<>();

   /**
    * Check if the given resource represents a system handle.  The procedure system handles 
    * (THIS-PROCEDURE, SOURCE-PROCEDURE, TARGET-PROCEDURE) and the window system handles 
    * (DEFAULT-WINDOW, ACTIVE-WINDOW and CURRENT-wINDOW) are not considered system handles by 
    * this API.
    * 
    * @param     res
    *            The resource to check.
    *            
    * @return    <code>true</code> if the resource represents a system handle as defined above.
    */
   public static boolean isSystemHandle(WrappedResource res)
   {
      return res.valid() && isSystemHandle(new handle(res));
   }
   
   /**
    * Get the list of valid events for the specified resource.
    * <p>
    * The order in which these are returned by 4GL has no meaning; in FWD, these are returned 
    * sorted alphabetically.
    * 
    * @param    h
    *           The handle referencing the resource.
    *           
    * @return   See above.
    */
   public static character listEvents(handle h)
   {
      UnimplementedFeature.missing("LIST-EVENTS");
      return new character();
   }
   
   /**
    * Get the list of valid events for the specified resource.
    * <p>
    * The order in which these are returned by 4GL has no meaning; in FWD, these are returned 
    * sorted alphabetically.
    * 
    * @param    h
    *           The handle referencing the resource.
    *           
    * @param    platform
    *           The display type (GUI or TTY are valid).
    *           
    * @return   See above.
    */
   public static character listEvents(handle h, String platform)
   {
      UnimplementedFeature.missing("LIST-EVENTS");
      return new character();
   }
   
   /**
    * Get the list of valid events for the specified resource.
    * <p>
    * The order in which these are returned by 4GL has no meaning; in FWD, these are returned 
    * sorted alphabetically.
    * 
    * @param    h
    *           The handle referencing the resource.
    *           
    * @param    platform
    *           The display type (GUI or TTY are valid).
    *           
    * @return   See above.
    */
   public static character listEvents(handle h, character platform)
   {
      UnimplementedFeature.missing("LIST-EVENTS");
      return new character();
   }
   
   /**
    * Get the list of queryable attributes and methods for the specified resource.
    * <p>
    * The order in which these are returned by 4GL has no meaning; in FWD, these are returned 
    * sorted alphabetically.
    * 
    * @param    h
    *           The handle referencing the resource.
    *           
    * @return   See above.
    */
   public static character listQueryAttributes(handle h)
   {
      if (!h._isValid())
      {
         return new character();
      }
      
      WrappedResource res = h.getResource();
      String qryAttrs = QUERY_ATTRIBUTES.get(res.getClass());

      if (qryAttrs != null)
      {
         return new character(qryAttrs);
      }
      
      Function<Method, String> legacy = (m ->
      {
         // legacy attribute setter methods are not excluded here, as this API is used as a 
         // helper to populate the cache (attribute to java method) method/getter/setter maps.
         // note that the attribute name will not be reported twice.
         
         final LegacyAttribute la = m.getAnnotation(LegacyAttribute.class);
         if (la != null && !la.ignore())
         {
            return la.name();
         }
         
         final LegacyMethod lm = m.getAnnotation(LegacyMethod.class);
         
         return lm != null && !lm.ignore() ? lm.name() : null;
      });
      
      qryAttrs = listLegacyResource(res, legacy, true);
      
      QUERY_ATTRIBUTES.put(res.getClass(), qryAttrs);
      
      return new character(qryAttrs);
   }

   /**
    * Get the list of settable attributes for the specified resource.
    * <p>
    * The order in which these are returned by 4GL has no meaning; in FWD, these are returned 
    * sorted alphabetically.
    * 
    * @param    h
    *           The handle referencing the resource.
    *           
    * @return   See above.
    */
   public static character listSetAttributes(handle h)
   {
      if (!h._isValid())
      {
         return new character();
      }
      
      WrappedResource res = h.getResource();
      String setAttrs = SET_ATTRIBUTES.get(res.getClass());

      if (setAttrs != null)
      {
         return new character(setAttrs);
      }
      
      Function<Method, String> legacy = (m ->
      {
         LegacyAttribute la = m.getAnnotation(LegacyAttribute.class);
         
         return (la != null && la.setter() && !la.ignore() ? la.name() : null);
      });
      
      setAttrs = listLegacyResource(res, legacy, false);
      
      SET_ATTRIBUTES.put(res.getClass(), setAttrs);
      
      return new character(setAttrs);
   }

   /**
    * Check if the given resource represents a system handle.  The procedure system handles 
    * (THIS-PROCEDURE, SOURCE-PROCEDURE, TARGET-PROCEDURE) and the window system handles 
    * (DEFAULT-WINDOW, ACTIVE-WINDOW and CURRENT-wINDOW) are not considered system handles by 
    * this API.
    * 
    * @param     h
    *            The handle to check.
    *            
    * @return    <code>true</code> if the handle references a system handle.
    */
   public static boolean isSystemHandle(handle h)
   {
      return h._isValid() && h.get() instanceof PseudoWidget;
   }
   
   /**
    * Deletes the given handles. If multiple handles refer the same resource, then the resource is
    * deleted only once (the first time is encountered).
    * <p>
    * In cases when the handle is a system handle, an {@link #invalidDelete()}
    * is called (to raise an error).
    * 
    * @param    handles
    *           The list of handles to be deleted.
    */
   public static void delete(handle... handles)
   {
      Set<Object> unique = new HashSet<>();
      for (handle h : handles)
      {
         if (h == null)
         {
            invalidDelete();
            break;
         }

         Object referent = h.get();
         if (unique.contains(referent))
            continue;
         
         unique.add(referent);
         
         if (!h._isValid())
         {
            // this is shown for all invalid handles.
            String msg = "Invalid handle.  Not initialized or points to a deleted object";
            ErrorManager.displayError(3135, msg);
            continue;
         }
         
         // DELETE WIDGET can't be protected with no error, so if something fails here, the error
         // will propagate
         delete(h);
      }
   }

   /**
    * Deletes the given handle. The current implementation allows only procedure handles to be
    * deleted.
    * <p>
    * In cases when the handle is a system handle, an {@link #invalidDelete()} is called (to raise
    * an error).
    *
    * @param   h
    *          The handle to be deleted.
    */
   public static void delete(handle h)
   {
      delete(h, true);
   }
   
   /**
    * Deletes the given handle. The current implementation allows only procedure handles to be
    * deleted.
    * <p>
    * In cases when the handle is a system handle, an {@link #invalidDelete()} is called (to raise
    * an error).
    *
    * @param   h
    *          The handle to be deleted.
    */
   public static void delete(handle h, boolean explicit)
   {
      if (h != null && h._isValid())
      {
         Object referent = h.get();
         if (referent instanceof CommonHandleTree)
         {
            handle child = h.unwrapTree().firstChild();
            while (child._isValid())
            {
               BaseConfig config = (BaseConfig) ((GenericWidget) child.get()).config();
               handle next = null;
               if (child.get() instanceof CommonHandleTree)
               {
                  next = child.unwrap().getNextSibling();
               }

               if (config.dynamic)
               {
                  delete(child);
               }
               
               if (next != null)
               {
                  child = next;
               }
               else
               {
                  break;
               }
            }
         }

         if (isSystemHandle(h) || 
             (ProcedureManager.systemHandle(referent)) && 
              !ProcedureManager._isPersistent(referent))
         {
            // persistent system handle procedure can be deleted!
            invalidDelete(h);
            return;
         }
         
         WrappedResource resource = h.getResource();
         if (resource instanceof WindowWidget && 
             resource == LogicalTerminal.defaultWindow().getResource())
         {
            // the default window is automatically created at startup and it cannot be destroyed.
            // Only in GUI mode, other WINDOWS can created and destroyed.
            ErrorManager.recordOrShowError(4140, "Cannot DESTROY default window", false, false);
            return;
         }
         
         if (resource instanceof Deletable)
         {
            if (((Deletable) resource).allowDelete(h, explicit))
            {
               ((Deletable) resource).delete();
            }
            
            // do not perform any cleanup here. delete() doesn't ensure the resource was deleted
            // (an error might be raised, with NO-ERROR clause present), so cleanup must be done
            // either in HandleChain.delete (if the resource extends HandleChain) or in resource's
            // delete implementation, if the resource can be deleted
            return;
         }
         
         throw new RuntimeException(
            "Delete implementation not available for handle " + 
            h.unwrapType().resourceType().toStringMessage());
      }
      
      if (h != null && !h._isValid())
      {
         WrappedResource resource = h.getResource();
         if (resource != null && resource instanceof MultiDeletable)
         {
            if (((MultiDeletable) resource).handleMultipleDeletes())
               return;
         }
      }
      invalidDelete(h);
   }

   /**
    * Reports if the given attribute or method can be read from the given handle. The 4GL 
    * documentation suggests this can only work on handles that are widgets, but this can be 
    * called on any handle.  This corresponds to the <code>CAN-QUERY()</code> built-in function.
    * 
    * @param    h
    *           The handle to be tested.
    * @param    attr
    *           The name of the attribute to be checked.
    *
    * @return   <code>true</code> if the resource in this handle supports reading the named
    *           attribute or method, <code>false</code> if the resource is valid but doesn't 
    *           support the named attribute and <code>unknown</code> if either input is 
    *           <code>unknown</code>. 
    */
   public static logical canQuery(handle h, character attr)
   {
      if (attr == null || attr.isUnknown())
      {
         return new logical(false);
      }
      
      return canQuery(h, attr.toStringMessage());
   }
   
   /**
    * Reports if the given attribute or method can be read from the given handle. The 4GL 
    * documentation suggests this can only work on handles that are widgets, but this can be 
    * called on any handle.  This corresponds to the <code>CAN-QUERY()</code> built-in function.
    * 
    * @param    h
    *           The handle to be tested.
    * @param    attributeOrMethodName
    *           The name of the attribute or method to be checked.
    *
    * @return   <code>true</code> if the resource in this handle supports reading the named
    *           attribute or method, <code>false</code> if the resource is valid but doesn't 
    *           support the named attribute and <code>unknown</code> if either input is 
    *           <code>unknown</code>. 
    */
   public static logical canQuery(final handle h, final String attributeOrMethodName)
   {
      if (h == null || h.isUnknown())
      {
         return logical.UNKNOWN;
      }

      final Object res = h.get();
      if (res == null)
      {
         return logical.FALSE;
      }

      final String upcased = ProgressAst._keyword(attributeOrMethodName).toUpperCase();

      if (isHandleAttribute(upcased))
      {
         return logical.TRUE;
      }

      if (res instanceof HandleChain && invalidChainAttribute((HandleChain) res, upcased, true))
      {
         return logical.FALSE;
      }

      // check getters first
      boolean canq = isLegacyResourceOperationAllowed(res, upcased, LEGACY_GETTERS, m -> 
      {
         LegacyAttribute annotation = m.getAnnotation(LegacyAttribute.class);
         return annotation != null && annotation.setter() ? null : annotation;
      });
      
      if (!canq)
      {
         // getter not found, check methods
         canq = isLegacyResourceOperationAllowed(res, 
                                                 upcased, 
                                                 LEGACY_METHODS, 
                                                 m -> m.getAnnotation(LegacyMethod.class));
      }
      
      return logical.of(canq);
   }
   
   /**
    * Reports if the given attribute can be written to the given handle. The 4GL documentation
    * suggests this can only work on handles that are widgets, but this can be called on any
    * handle.  This corresponds to the <code>CAN-SET()</code> built-in function.
    * 
    * @param    h
    *           The handle to be tested.
    * @param    attr
    *           The name of the attribute to be checked.
    *
    * @return   <code>true</code> if the resource in this handle supports writing the named
    *           attribute, <code>false</code> if the resource is valid but doesn't support
    *           the named attribute and <code>unknown</code> if either input is 
    *           <code>unknown</code>. 
    */
   public static logical canSet(handle h, character attr)
   {
      if (attr == null || attr.isUnknown())
      {
         return new logical(false);
      }
      
      return canSet(h, attr.toStringMessage());
   }
   
   /**
    * Reports if the given attribute can be written to the given handle. The 4GL documentation
    * suggests this can only work on handles that are widgets, but this can be called on any
    * handle.  This corresponds to the <code>CAN-SET()</code> built-in function.
    * 
    * @param    h
    *           The handle to be tested.
    * @param    attributeName
    *           The name of the attribute to be checked.
    *
    * @return   <code>true</code> if the resource in this handle supports writing the named
    *           attribute, <code>false</code> if the resource is valid but doesn't support
    *           the named attribute and <code>unknown</code> if either input is 
    *           <code>unknown</code>. 
    */
   public static logical canSet(final handle h, final String attributeName)
   {
      if (h == null || h.isUnknown())
      {
         return new logical();
      }

      final String attributeUpcased = attributeName.toUpperCase();
      if (isHandleAttribute(attributeUpcased))
      {
         return new logical(false);
      }

      final Object res = h.get();
      if (res instanceof HandleChain
               && invalidChainAttribute((HandleChain) res, attributeUpcased, false))
      {
         return new logical(false);
      }

      return new logical(isLegacyResourceOperationAllowed(res, attributeUpcased, LEGACY_SETTERS, m -> {
         final LegacyAttribute annotation = m.getAnnotation(LegacyAttribute.class);
         return annotation != null && annotation.setter() ? annotation : null;
      }));
   }
   
   /**
    * Find if the legacy attribute or method can be used with the resource referenced by the
    * given handle.
    * <p>
    * This will use the global {@link #SETTERS_TO_JAVA}, {@link #GETTERS_TO_JAVA} and 
    * {@link #METHODS_TO_JAVA} maps - if the cached values do not exist for the given resource
    * type, this will be initialized via a {@link #listQueryAttributes} call.
    * @param    h
    *           The resource.
    * @param    attrOrMethod
    *           The attribute or method name.
    * @param    attr
    *           Flag indicating if looking for attributes only.
    * @param    setter
    *           Flag indicating if looking for attribute setters only.
    */
   static String findLegacy(handle h, String attrOrMethod, boolean attr, boolean setter)
   {
      String upcased = attrOrMethod.toUpperCase();
      Map<String, String> jnames = attr ? (setter ? SETTERS_TO_JAVA : GETTERS_TO_JAVA) 
                                        : METHODS_TO_JAVA;
      String mthd = jnames.get(upcased);
      
      if (mthd == null)
      {
         // one more try, in case this resource wasn't initialized yet
         
         WrappedResource res = h.getResource();
         if (!QUERY_ATTRIBUTES.containsKey(res.getClass()))
         {
            listQueryAttributes(h);
         }
         
         mthd = jnames.get(upcased);
         
         // if is null here, it can't be found
      }
      
      return mthd;
   }

   /**
    * Checks if the given attribute name is HANDLE.  This is used to bypass reflection based code
    * to find out if the attribute can be set or queried.  We know this is read-only and always
    * exists for valid handle we use in CAN-SET and CAN-QUERY calls.
    * 
    * @param    attrName
    *           The name of the attribute to be checked.
    *
    * @return   <code>true</code> if the attribute name is HANDLE, <code>false</code> otherwise.
    */
   private static boolean isHandleAttribute(String attrName)
   {
      return attrName.equalsIgnoreCase("HANDLE");
   }
   
   /**
    * Invoked when the handle is invalid or a system (non-procedure) handle. 
    * Shows appropriate error messages and raises an error. 
    * Calls {@link #invalidDelete(handle) invalidDelete(null)}.
    */
   private static void invalidDelete()
   {
      invalidDelete(null);
   }

   /**
    * Invoked when the handle is invalid or a system (non-procedure) handle. 
    * Shows appropriate error messages and raises an error.
    * 
    * @param    h
    *           The handle for which the delete failed. May be 
    *           <code>null</code> if it was decide this is a non-procedure 
    *           handle.
    */
   private static void invalidDelete(handle h)
   {
      boolean procedure = (h != null && h._isValid() && ProcedureManager.isProcedure(h));

      final String msg1 = "Invalid or inappropriate handle value given to " +
                          "DELETE OBJECT or DELETE PROCEDURE statement";
      final int id1 = 5425;

      final String msg2 = "Invalid or inappropriate handle value given to " +
                          "DELETE PROCEDURE statement. Procedure '%s':%d";
      final int id2 = 1428;
      // this can't be computed, is the legacy line num from the 4GL file.
      final int linenum = 0;
      
      int id = -1;
      String msg = null;
      
      if (procedure)
      {
         String pname = ProcedureManager.getRelativeName(h.get());
         msg = String.format(msg2, pname, linenum);
         id = id2;
      }
      else
      {
         msg = msg1;
         id = id1;
      }

      if (LOG.isLoggable(Level.FINE))
      {
         String emsg = (h == null ? "with null handle" 
                                  : (h.get() == null ? "with null resource" 
                                                     : "with resource " + h.get().getClass()));
         String noError = ErrorManager.isSilentError() ? "(with NO-ERROR) " : "";
         emsg = msg + ". (" + id + ") - " + noError + emsg;
         
         LOG.log(Level.FINE, emsg, new ErrorConditionException(emsg));
      }
      
      ErrorManager.recordOrThrowError(id, msg, false);
   }
   
   /**
    * Get a listing of the specified resource, of all its attributes/methods (in 'query' mode)
    * or all its setable attributes (in non-query mode).
    * 
    * @param    res
    *           The resource for which to resolve the attributes/methods.
    * @param    resolver
    *           Function which checks if a method can be used by this call.
    * @param    query
    *           Flag indicating if we are looking for queryable attributes/methods (when true) or
    *           only settable attributes (when false).
    *            
    * @return   The alphabetically ordered list of attributes and methods.
    */
   private static String listLegacyResource(Object                   res, 
                                            Function<Method, String> resolver, 
                                            boolean                  query)
   {
      Class<?> cls = res.getClass();
      List<Method> methods = listAllMethods(cls);
      methods.addAll(StaticProxy.getStaticMethods(res));
      
      boolean isHandleChain = (res instanceof HandleChain);
      HandleChain chain = (isHandleChain ? (HandleChain) res : null);
      Set<String> names = new TreeSet<>();
      for (Method mthd : methods)
      {
         String name = resolver.apply(mthd);
         if (name != null)
         {
            Map<String, String> jnames;

            if (mthd.isAnnotationPresent(LegacyAttribute.class))
            {
               LegacyAttribute lattr = mthd.getAnnotation(LegacyAttribute.class);

               if (isHandleChain && invalidChainAttribute(chain, name, query))
               {
                  continue;
               }
               
               jnames = lattr.setter() ? SETTERS_TO_JAVA : GETTERS_TO_JAVA;
            }
            else
            {
               jnames = METHODS_TO_JAVA;
            }
            
            String jname = jnames.get(name);
            if (jname != null && !jname.equals(mthd.getName()))
            {
               LOG.warning("An attribute/method is defined with more than one Java method name: " + 
                           jname + " " + mthd);
            }

            jnames.put(name, mthd.getName());

            names.add(name);
         }
      }
      
      if (query)
      {
         names.add("HANDLE");
      }
      
      String q = "";
      for (String name : names)
      {
         q = q + (q.isEmpty() ? "" : ",") + name;
      }
      
      return q;
   }

   /**
    * List all methods declared in this class and all its super-classes and interfaces.
    *  
    * @param    cls
    *           The class which requires its methods listed.
    *           
    * @return   See above.
    */
   private static List<Method> listAllMethods(Class<?> cls)
   {
      List<Method> allMethods = new ArrayList<>();
      
      Class<?> current_base = cls; 
      while (current_base != null)
      {
         allMethods.addAll(Arrays.asList(current_base.getDeclaredMethods()));

         Class<?>[] ifaces = current_base.getInterfaces();
         if (ifaces != null)
         {
            for (Class<?> iface : ifaces)
            {
               allMethods.addAll(listAllMethods(iface));
            }
         }

         current_base = current_base.getSuperclass(); 
      }
      
      return allMethods;
   }
   
   /**
    * Check if the specified chain attribute is invalid with the given resource, in query or 
    * settable mode.
    * 
    * @param    chain
    *           The chainable resource.
    * @param    name
    *           The attribute name.
    * @param    query
    *           Check for reading (when <code>true</code>) or writing (when <code>false</code>).
    *           
    * @return   <code>true</code> if the attribute is invalid in this resource.
    */
   private static boolean invalidChainAttribute(HandleChain chain, String name, boolean query)
   {
      switch (name)
      {
         case "PREV-SIBLING":
            return !chain.hasPrevSibling();
         case "NEXT-SIBLING":
            return !chain.hasNextSibling();
         case "PRIVATE-DATA":
            return !chain.hasPrivateData();
         case "NAME":
            return !chain.hasName() || (!query && chain.hasNameReadOnly());
         case "SCREEN-VALUE":
            return chain instanceof WindowWidget ||
                   chain instanceof FrameWidget  ||
                   chain instanceof FieldGroup;
         default:
            return false;
      }
   }
   
   /**
    * Get the set of operations of given type allowed for a class instances.
    * 
    * Note: since this method is only called from a synchronized section
    * already, there is no point bother of synchronization in this method.
    * 
    * @param fromClass
    *        the class or interface in question
    * @param table
    *        
    * @param resolver
    *        a function which takes a method as the only arguments and
    *        returns a method annotation, matching some condiotions,
    *        or {@code null} if no method annotation matches.
    * 
    * @return the set of operation names for the given method arguments
    */
   private static final Set<String> legacyResourceAllowedOperations(Class<?>                     fromClass,
                                                                    Map<String, Set<String>>     table,
                                                                    Function<Method, Annotation> resolver)
   {
      final String className = fromClass.getName();

      Set<String> annotations = table.get(className);
      if (annotations != null)
      {
         return annotations;
      }

      annotations = new HashSet<>();

      /**
       * Collect data from all interfaces and the superclass,
       * we are not expecting the name intersections. 
       */
      for (Class<?> iface : fromClass.getInterfaces())
      {
         annotations.addAll(legacyResourceAllowedOperations(iface, table, resolver));
      }

      Class<?> superClass = fromClass.getSuperclass();
      if (superClass != null && superClass != Object.class)
      {
         annotations.addAll(legacyResourceAllowedOperations(superClass, table, resolver));
      }

      Set<String> annos = annotations;
      Utils.dfsClassHierarchy(fromClass, (cls) ->
      {
         // do a DFS walk, and add/remove the annotations based on if there is a method defined for it, and
         // the state of the 'ignore' flag at the annotation.  a granpa class can set it to 'ignore', and 
         // another parent class can set it back to 'allowed'.
         Method[] mthds = cls.getDeclaredMethods();
         legacyResourceAllowedUpdateFromMethods(Arrays.asList(mthds), annos, resolver);
      });

      if (annotations.isEmpty())
      {
         annotations = Collections.emptySet();
      }

      table.put(className, annotations);

      return annotations;
   }
   
   /**
    * Test if a legacy operation is allowed for a given resource based on method annotations.
    * <p>
    * Obtain the legacy attribute annotation instance for the given handle object and given
    * legacy attribute name.
    * <p>
    * The search algorithm for this method uses reflection to read annotations from various
    * sources in the following precedence order:
    * <ol>
    *   <li> The proxy static methods of the resource itself if exist.
    *   <li> The class of the resource itself.
    *   <li> The superclass of the resource or.
    *   <li> All interfaces of the resource.
    * </ol>
    * <p>
    * The search is repeated recursively for the superclass and interfaces.
    * <p>
    * Where possible, we put annotations on the interface, not the class for the resource.  But
    * there are some attributes where the configuration is not universal for all resources
    * implementing the same attribute.  For example, the following attributes are readable for
    * certain resources even though for most other resources the same attribute is writable:
    * <ul>
    *   <li> LABEL for LAST-EVENT
    *   <li> MODIFIED for browse columns
    *   <li> COLUMN for browse cells, field groups, and LAST-EVENT
    *   <li> NO-VALIDATE for both static and dynamic browses (it is writeable only for dynamic
    *        browses)
    * </ul>
    * <p>
    * Keeping the above precedence order is critical to allow attribute configurations of
    * more-specific resources to properly override the more general configuration in the parent
    * hierarchy or the interface hierarchy.
    *
    * @param resource
    *        the resource jbject to test
    * @param key
    *        the operation name, must be in upper case
    * @param table
    *        the map holding sets of allowed operations for an operation type, this map
    *        is updated lazily in this call
    * @param resolver
    *        a function which takes a method as the only arguments and
    *        returns a method annotation, matching some condiotions,
    *        or {@code null} if no method annotation matches.
    * 
    * @return   See above.
    */
   private static boolean isLegacyResourceOperationAllowed(Object                       resource,
                                                           String                       key,
                                                           Map<String, Set<String>>     table,
                                                           Function<Method, Annotation> resolver)
   {
      boolean resourceIsProxy = StaticProxy.isStaticProxy(resource);
      Class<?> klass = resource.getClass();
      if (resourceIsProxy)
      {
         klass = klass.getInterfaces()[0];
      }
      String tableKey = klass.getName();

      Set<String> allowed = table.get(tableKey);
      if (allowed == null)
      {
         synchronized (table)
         {
            if (allowed == null)
            {
               allowed = legacyResourceAllowedOperations(klass, table, resolver);
            }
         }
      }

      return allowed.contains(key);
   }

   /**
    * Update the set of allowed names based on methods annotations.
    * 
    * This method scans all methods provided for method annotations of
    * the given class, and, if found, adds or removes entries in the
    * provided set.
    * 
    * @param methods
    *        the methods to scan for annotations.
    * @param allowSet
    *        the set of allowed names to update
    * @param resolver
    *        a function which takes a method as the only arguments and
    *        returns a method annotation, matching some condiotions,
    *        or {@code null} if no method annotation matches.
    */
   private static void legacyResourceAllowedUpdateFromMethods(
            final Iterable<Method>             methods,
            final Set<String>                  allowSet,
            final Function<Method, Annotation> resolver)
   {
      for (Method method : methods)
      {
         final Annotation anno = resolver.apply(method);
         if (anno == null)
         {
            continue;
         }

         /**
          * Extract name and allow info from the annotation
          */
         String name;
         boolean allow;
         if(anno instanceof LegacyAttribute)
         {
            LegacyAttribute attAnno = (LegacyAttribute) anno;
            name = attAnno.name();
            allow = !attAnno.ignore();
         }
         else
         {
            LegacyMethod methAnno = (LegacyMethod) anno;
            name = methAnno.name();
            allow = !methAnno.ignore();
         }            

         /**
          * Update the set of allowed operations.
          */
         if (allow)
         {
            allowSet.add(name);
         }
         else
         {
            allowSet.remove(name);
         }
      }
   }   
}