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