CallbackResolver.java
/*
** Module : CallbackResolver.java
** Abstract : provides a dictionary of method callbacks to implement user-
** defined functions without tight coupling between caller and
** called
**
** Copyright (c) 2004-2023, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- ----------------------------Description-----------------------------
** 001 GES 20041116 @18655 First version, with support for resolving:
** - optionally abbreviated keywords
** representing the function name to match
** - static and instance method callbacks
** - method resolution via reflection
** 002 GES 20041122 @18719 Minor changes to allow for overloaded
** methods (within the same target class):
** - removed the caching of Method instances
** in lookupCallback()
** - modified the javadoc to explain the
** situations in which overloading will work
** and the technique by which one can use it
** This was needed to fully handle the defined()
** preprocessor function.
** 003 GES 20070319 @32497 Improved error handling code. Provided new
** support for invoking constructors. Moved
** away from use of certain J2SE reflection
** interfaces in favor of more appropriate
** search algorithms that are implemented in
** ReflectionHelper.
** 004 IAS 20160331 Fixed potential NPE
** 005 GES 20190929 Added a copy constructor.
** 006 TJD 20220504 Java 11 compatibility minor changes
** 007 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
*/
/*
** 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.uast;
import com.goldencode.p2j.convert.*;
import com.goldencode.p2j.util.*;
import com.goldencode.util.*;
import java.io.*;
import java.lang.reflect.*;
import java.util.*;
import java.util.logging.*;
/**
* Provides a mechanism for storage and retrieval of method callbacks as
* represented by the <code>{@link Callback}</code> object. Each callback is
* stored in a dictionary and can be found by a literal match with a function
* name or can be optionally matched with a set of valid function name
* abbreviations.
* <p>
* The namespace for the dictionary is flat. For one function name that is
* matched in the resolver dictionary, there is only one <code>Callback</code>
* object that can be found. This means that any features that are coded
* in a callback definition must be implemented the same way for all target
* methods. This is especially important when overloading function names.
* <p>
* Limitations on function overloading:
* <ul>
* <li> All overloaded functions must be implemented in the same instance
* object (they can resolve to any parent in the class hierarchy or
* to the class of the instance) if non-static.
* <li> If the method is defined as static, all overloaded functions must
* be in the same class.
* <li> All overloaded functions need to be static or all non-static
* since there is only one instance object in a callback definition
* and this instance determines how the method is called (if it is
* <code>null</code> it is static, otherwise it is used as the
* instance for the method call.
* <li> Overloading *cannot* be done using the
* <code>{@link #addCallback(Keyword,Object,Method)}</code> method
* since this hardcodes the <code>Method</code> object to be used
* for the invocation. In this case, the same exact method will
* *always* by invoked.
* <li> To implement overloading, use the
* <code>{@link #addCallback(Keyword,Object,String,String)}</code>
* method. This delays the resolution of the <code>Method</code>
* object until <code>{@link #lookupCallback}</code> is invoked
* which is the time at which the proper signature is known. Each
* time the lookup is done, the signature that is passed in is used
* to resolve to the correct target method with the same name as
* that coded in the callback definition.
* <li> Due to the flat namespace, there is only one mapping of a keyword
* to a method. This means that for every Progress keyword that must
* be overloaded, there is only one <code>Keyword</code> object that
* can be found and this single object can have only 1 associated
* target method name. This single method name must have 1 signature
* for every match to a valid signature that will be found in the
* Progress function call.
* </ul>
* WARNING: this class is NOT thread-safe!!!! In particular, the
* <code>invokeCallback</code> method reads a callback definition and then
* calculates and uses some information from it. There is an inherent
* race condition in between obtaining and using this callback information.
*
* @author GES
*/
public class CallbackResolver
{
/** Logger */
private static final ConversionStatus LOG = ConversionStatus.get(CallbackResolver.class);
/**
* Stores the set of all <code>Keywords</code> with each keyword
* corresponding to a specific <code>Callback</code> object.
*/
private KeywordDictionary kwDict;
/**
* Default constructor which instantiates a <code>CallbackResolver</code>
* object which matches on a case-insensitive basis.
*/
public CallbackResolver()
{
this(false);
}
/**
* Instantiates a <code>CallbackResolver</code> object.
*
* @param matchCase
* Controls lookup algorithm case-sensitivity.
*/
public CallbackResolver(boolean matchCase)
{
kwDict = new KeywordDictionary(matchCase);
}
/**
* Copy constructor, makes an independent duplicate of the given instance.
*
* @param other
* Instance to duplicate.
*/
public CallbackResolver(CallbackResolver other)
{
this.kwDict = new KeywordDictionary(other.kwDict);
}
/**
* Creates a <code>{@link Callback}</code> object using the
* {@link Callback#Callback(Object,String,String)} constructor and then
* stores this instance in the <code>Keyword</code> object and adds this
* associated <code>Keyword</code> object to the dictionary.
* <p>
* This method MUST be used to add an overloaded function to the
* dictionary, since this is the only way to add a callback definition in
* which the signature of the target is NOT hard coded.
*
* @param word
* Keyword object to add. Must not be <code>null</code>.
* @param instance
* Target object on which to invoke the callback or
* <code>null</code> if the method is static.
* @param className
* Fully qualified class name of the target object's class.
* Must not be <code>null</code>.
* @param methodName
* Method name of the method to invoke on the target object.
* Use <code>null</code> or an empty string to specify a
* constructor.
*/
public void addCallback(Keyword word,
Object instance,
String className,
String methodName)
{
word.setExtra(new Callback(instance, className, methodName));
kwDict.addKeyword(word);
}
/**
* Creates a <code>{@link Callback}</code> object using the
* {@link Callback#Callback(Object,Method)} constructor and then
* stores this instance in the <code>Keyword</code> object and adds this
* associated <code>Keyword</code> object to the dictionary.
* <p>
* This method CANNOT be used to add an overloaded function to the
* dictionary, since the signature of the target is hard coded into the
* <code>Method</code> object. Use
* <code>{@link #addCallback(Keyword,Object,String,String)}</code>
* instead.
*
* @param word
* Keyword object to add.
* @param instance
* Target object on which to invoke the callback or null if the
* method is static.
* @param method
* Method object which can be used to invoke the target
* method via reflection.
*/
public void addCallback(Keyword word,
Object instance,
Method method)
{
word.setExtra(new Callback(instance, method));
kwDict.addKeyword(word);
}
/**
* Processes a dictionary lookup for a specified <code>Callback</code>.
* If a match is found, the associated <code>Callback</code> object is
* returned. Otherwise <code>null</code> is returned.
* <p>
*
* @param funcName
* The function name to be used as a key in the lookup. If case-
* sensitivity is off, this may be in any case (including
* mixed case) and the result will always match.
* @return A <code>Callback</code> object or null if no match was found.
*/
public Callback lookupCallback(String funcName)
{
Keyword word = kwDict.lookupKeyword(funcName);
if (word == null)
{
// no possible matches
return null;
}
return (Callback) word.getExtra();
}
/**
* Lookup the callback definition associated with the function name and
* invoke the target method using the passed arguments.
* <p>
* The lookup function will find the callback definition, if it exists.
* <p>
* Once obtained, the <code>Callback.getMethod</code> will be called. If
* it returns a valid <code>Method</code> object then the caller-passed
* arguments are *not* checked and this <code>Method</code> is used. For
* this reason function overloading cannot be used when the callback
* definition has a hard-coded <code>Method</code> object and no class
* and method names encoded.
* <p>
* If {@link Callback#getMethod} is called and the method is
* <code>null</code>, then the reflection services are used to obtain a
* <code>java.lang.reflect.Method</code> object based on the class name,
* method name and the signature definition passed in as a parameter by
* the caller.
* <p>
* Once the <code>Method</code> is obtained, reflection is used to invoke
* that method and the arguments array is passed.
* <p>
* NO caching of the <code>Method</code> object is done at the time of
* a lookup. This allows one to code a callback definition using only
* the class and method names and then to defer all signature processing
* until this lookup method is called. In this case, the signature
* passed into the lookup is used to resolve the target method using
* reflection and if the correct method exists it will be found.
*
* @param funcName
* The function name to be used as a key in the lookup. If case-
* sensitivity is off, this may be in any case (including
* mixed case) and the result will always match.
* @param returnType
* The <code>Class</code> object representing the class of the
* method's return type.
* @param signature
* An array of <code>Class</code> objects representing the class
* of each parameter in addition to the sequence of the
* parameters.
* @param args
* An array of <code>Object</code> objects representing each
* parameter in the proper sequence expected by the target
* method.
* @return Result from the target method invocation.
*/
public Object invokeCallback(String funcName,
Class returnType,
Class[] signature,
Object[] args)
throws ReflectiveOperationException,
NullPointerException
{
Callback cb = lookupCallback(funcName);
if (cb == null)
{
String sig = ReflectionHelper.toStringSignature(funcName,
signature,
returnType);
LOG.log(Level.SEVERE, "Missing function or variable " + sig);
return null;
}
// honor any method instance that was explicitly provided by the caller
Method method = cb.getMethod();
if (method == null)
{
// attempt to find a match dynamically
String ctxt = cb.getClassName();
String mtxt = cb.getMethodName();
Class cls = Class.forName(ctxt);
// null or empty method name is a constructor
if (mtxt == null || mtxt.length() == 0)
{
// lookup a constructor
Constructor cmethods[] = cls.getConstructors();
Constructor found = null;
// we must manually lookup the constructor since the J2SE 1.4.x
// Class.getConstructor() doesn't check for assignability so
// a signature based on child classes won't match a compatible
// signature that uses super classes
outer:
for (int j = 0; j < cmethods.length; j++)
{
Class[] possible = cmethods[j].getParameterTypes();
if (ReflectionHelper.testSignature(possible, signature))
{
found = cmethods[j];
break outer;
}
}
return found != null ? found.newInstance(args) : null;
}
else
{
// normal method
Method methods[] = cls.getMethods();
// we must manually lookup the constructor since the J2SE 1.4.x
// Class.getMethod() doesn't check for assignability so
// a signature based on child classes won't match a compatible
// signature that uses super classes
outer:
for (int j = 0; j < methods.length; j++)
{
if (methods[j].getName().equals(mtxt))
{
Class[] possible = methods[j].getParameterTypes();
if (ReflectionHelper.testSignature(possible, signature))
{
method = methods[j];
break outer;
}
}
}
if (method == null)
{
String sig = ReflectionHelper.toStringSignature(mtxt,
signature,
returnType);
LOG.log(Level.SEVERE, "Method '" + sig + "' not found.");
}
}
// DO NOT cache this for possible use later since this would
// disable overloaded function support!!!
}
return method == null ? null : method.invoke(cb.getTargetInstance(), args);
}
/**
* Prints a report to <code>stderr</code> which includes 1 line per
* entry in the dictionary. If a <code>Callback</code> object has an
* associated <code>Keyword</code> that supports abbreviations, then
* multiple entries will be found that refer to the same
* <code>Keyword</code>.
*/
public void dump()
{
StringOutputStream outputStream = new StringOutputStream();
PrintStream ps = new PrintStream(outputStream);
ps.println( "\n-------------Function Listing-------------" );
kwDict.dump(ps);
LOG.log(Level.INFO, outputStream.getData());
}
/**
* The Lexer and Parser that processes an expression must be aware of the
* same <code>Keyword</code> objects as this instance. This helper method
* obtains the list of <code>Keyword</code> objects from the keyword
* dictionary and adds each to the <code>SymbolResolver</code> to
* properly populate it.
*
* @param sym
* Object to populate with full list of <code>Keywords</code>.
*/
public void populateSymbolResolver(SymbolResolver sym)
{
Iterator elements = kwDict.keywordList();
while (elements.hasNext())
{
sym.addKeyword((Keyword) elements.next());
}
}
}