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