InternalEntry.java

/*
** Module   : InternalEntry.java
** Abstract : stores metadata relating to an internal entry 
**
** Copyright (c) 2013-2023, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
** 001 GES 20131124 Moved this code out of SourceNameMapper into its own class. This enables
**                  cleaner separation and modularity. Added synchronization and made changes
**                  for RETURN support.
** 002 ECF 20140918 Reduced memory footprint.
** 003 ECF 20150424 Minor memory tweak.
** 004 ECF 20150601 Minor performance fix.
** 005 OM  20150717 Fixed range check for parameters.
** 006 ECF 20150802 Removed synchronization, which was a performance bottleneck. Made constructors
**                  protected to ensure instances are not created elsewhere, other than by
**                  subclasses which presumably will be written with an understanding of thread
**                  safety. See class description notes for explanation of why unsynchronized
**                  methods are safe.
** 007 CA  20160418 Added getter for "parameters" field.
** 008 ECF 20180818 Avoid use of expensive String.format.
** 009 CA  20181029 Exposed setParameterList() and initialize() to sub-classes.
** 010 CA  20181216 Fixed loading OO methods, constructors and destructors.
** 011 CA  20190219 OO members (methods, constructors, class events) are annotated via the 
**                  LegacySignature and statically linked with the converted Java method.
**     CA  20190324 Cache the libname and mapTo values.
** 012 CA  20190520 Added some more OO method types, for internal FWD usage.
** 013 CA  20190604 Fixed a typo for LENGTH-OF name.
** 014 ME  20200518 Added VARIABLE and PROPERTY.
** 015 CA  20200605 The Java method instance is resolved on first access of getMethod().
** 016 ME  20200810 Added qualified name support in signature processing.
**     CA  20210218 Fixed signature processing for qualified return type.
**     CA  20220909 Fixed TABLE-HANDLE, DATASET, DATASET-HANDLE parameters reported by GET-SIGNATURE.
** 017 HC  20230125 Added caching of resolved procedure internal entries.
** 018 CA  20230928 Small optimization in 'initialize', user plain Java for instead of 'for each'.
** 019 CA  20231213 The synthetic 'execute' methods emitted for legacy classes can be dropped if there is 
**                  nothing emitted in them.  These Java methods are also marked with Type.EXECUTE
**                  LegacySignature annotation, and their BlockManager API removed - the FWD runtime will run
**                  this only once.
*/

/*
** 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 java.lang.reflect.*;
import java.util.*;

import com.goldencode.p2j.util.SourceNameMapper.ExternalProgram;

/**
 * Container for legacy info belonging to an internal entry (a user-defined internal procedure,
 * function or external API call).
 * <p>
 * The methods of this class are unsynchronized because the methods of this class are invoked
 * very frequently and testing and profiling indicates that a synchronized architecture creates
 * a bottleneck. Instances of this class are only (and MUST only) be constructed and initialized
 * from within synchronized code within {@link SourceNameMapper}. Although there are public
 * methods in this class which access internal state, they are only invoked after all internal
 * state has been set. Subclasses must be aware of this restriction to ensure they are thread
 * safe.
 */
public class InternalEntry
{
   /** Type of internal entry */
   public static enum Type
   {
      /** User defined function */
      FUNCTION("FUNCTION"),
      
      /** Internal procedure */
      PROCEDURE("PROCEDURE"),
      
      /** Object method */
      METHOD("METHOD"),
      
      /** Class event */
      EVENT("EVENT"),

      /** Event's subscribe method definition */
      SUBSCRIBE("SUBSCRIBE"),

      /** Event's unsubscribe method definition */
      UNSUBSCRIBE("UNSUBSCRIBE"),

      /** Event's publish method definition */
      PUBLISH("PUBLISH"),

      /** Property's getter method. */
      GETTER("GET"),

      /** Property's setter method. */
      SETTER("SET"),

      /** Extent property's resize method. */
      RESIZE("RESIZE"),

      /** Extent property's length-of method. */
      LENGTH("LENGTH-OF"),
      
      /** Class constructor */
      CONSTRUCTOR("CONSTRUCTOR"),
      
      /** Class destructor */
      DESTRUCTOR("DESTRUCTOR"),
      
      /** Native procedure */
      DLL_ENTRY("DLL-ENTRY"),
      
      /** User defined function defined in another procedure */
      EXTERN("EXTERN"),
      
      /** Main procedure */
      MAIN("MAIN"),
      
      /** Synthetic 'execute' methods emitted for legacy classes. */
      EXECUTE("EXECUTE"),
      
      /** Object variable */
      VARIABLE("VARIABLE"),
      
      /** Object property */
      PROPERTY("PROPERTY"),
      ;
      
      /** Map of Progress entry names to enum values */
      private static Map<String, Type> map = new HashMap<>(8);
      
      static
      {
         for (Type t : Type.values())
         {
            map.put(t.name, t);
         }
      }
      
      /** Progress entry name */
      private final String name;
      
      /**
       * Constructor.
       * 
       * @param   name
       *          Progress entry name.
       */
      Type(String name)
      {
         this.name = name.intern();
      }
      
      /**
       * Given a Progress entry name, retrieve the appropriate enum value.
       * 
       * @param   name
       *          Progress entry name.
       * 
       * @return  Associated enum value.
       * 
       * @throws  IllegalArgumentException
       *          if the given Progress entry name is unrecognized.
       */
      public static Type fromString(String name)
      {
         Type t = map.get(name);
         if (t != null)
         {
            return t;
         }
         
         throw new IllegalArgumentException("Unrecognized InternalEntry type: " + name);
      }
      
      /**
       * Return the Progress entry name.
       * 
       * @return  Progress entry name.
       */
      public String toString()
      {
         return name;
      }
   };
   
   public static enum SerializeMode {
      DEFAULT,
      VISIBLE,
      HIDDEN
   }
      
   /** Identify parameters in INPUT mode. */
   public static final char INPUT_MODE = 'I';
   
   /** Identify parameters in INPUT-OUTPUT mode. */
   public static final char INPUT_OUTPUT_MODE = 'U';
   
   /** Identify parameters in OUTPUT mode. */
   public static final char OUTPUT_MODE = 'O';
   
   /** Identify buffer parameters. */
   public static final char BUFFER_MODE = 'B';
   
   /** Legacy 4GL name for this internal-entry. */
   final String pname;
   
   /** Converted Java name for this internal-entry. */
   final String jname;
   
   /** The type of this internal entry. */
   private final Type type;
   
   /**
    * The signature of this internal entry, as returned by the
    * <code>get-signature</code> 4GL API.
    */
   private String signature = null;
   
   /** The string representation of the parameter modes for this internal entry. */
   private String parameterModes = null;
   
   /** The parameter list of this internal entry. */
   private List<Parameter> parameters = Collections.emptyList();
   
   /** A map containing all the internal entry's attributes. */
   private Map<String, String> attributes = new HashMap<>(4);
   
   /** 
    * Flag indicating if this is a IN SUPER internal entry (when set to <code>true</code>).
    * Gets its permanent value when internal entry is initialized by {@link SourceNameMapper}.
    */
   private Boolean isSuper = null;
   
   /** 
    * Flag indicating if this is a IN handle internal entry (when set to <code>true</code>).
    * Gets its permanent value when internal entry is initialized by {@link SourceNameMapper}.
    */
   private Boolean inHandle = null;
   
   /** 
    * Flag indicating if this is a PRIVATE internal entry (when set to <code>true</code>).
    * Gets its permanent value when internal entry is initialized by {@link SourceNameMapper}.
    */
   private Boolean isPrivate = null;
   
   /** The return type's extent. */
   private int extent = SourceNameMapper.NO_EXTENT;
   
   /** The qualified OO name. */
   private String qualified = null;
   
   /** The converted Java method from where this internal entry was built, using the 
    * {@link LegacySignature} annotation.
    */
   private Method method = null;
   
   /** The OS library name, if this is a native method. */
   private String libname = null;
   
   /** The MAP-TO option, if it applies. */
   private String mapTo = null;
   
   /**
    * The external program containing this entry.  It is non-null only for external programs, until the 
    * {@link #method} is resolved.
    */
   private ExternalProgram extprog;

   /** Represents null InternalEntry value */
   public static final InternalEntry NULL = new InternalEntry();

   /**
    * Private constructor for the internal implementation.
    */
   private InternalEntry()
   {
      pname = null;
      jname = null;
      type = null;
   }
   
   /**
    * Basic c'tor.
    * 
    * @param    pname
    *           Legacy 4GL name for this internal-entry.
    * @param    jname
    *           Converted Java name for this internal-entry.
    * @param    type
    *           The type of this internal entry.
    */
   protected InternalEntry(String pname, String jname, String type)
   {
      this(pname, jname, Type.fromString(type));
   }
   
   /**
    * Basic c'tor.
    * 
    * @param    pname
    *           Legacy 4GL name for this internal-entry.
    * @param    jname
    *           Converted Java name for this internal-entry.
    * @param    type
    *           The type of this internal entry.
    */
   protected InternalEntry(String pname, String jname, Type type)
   {
      this.pname = pname.intern();
      this.jname = jname.intern();
      this.type = type;
   }
   
   /**
    * Check if the given mode is an OUTPUT mode (i.e. {@link #OUTPUT_MODE} or 
    * {@link #INPUT_OUTPUT_MODE}.
    * 
    * @return   <code>true</code> if the given character represents one of the output
    *           modes.
    */
   public static boolean isOutputMode(char mode)
   {
      return mode == OUTPUT_MODE || mode == INPUT_OUTPUT_MODE;
   }
   
   /**
    * Get the OS library name, if it applies.
    * 
    * @return   The {@link #libname}.
    */
   public String getLibname()
   {
      return libname;
   }
   
   /**
    * Get the MAP-TO option, if it applies.
    * 
    * @return   The {@link #mapTo} option.
    */
   public String getMapTo()
   {
      return mapTo;
   }
   
   /**
    * Return <code>true</code> if this internal-entry has its <code>in-super</code> attribute set
    * to <code>"true"</code>.
    * 
    * @return   See above.
    */
   public boolean isSuper()
   {
      return isSuper;
   }
   
   /**
    * Return <code>true</code> if this internal-entry has its 
    * <code>private</code> attribute set to <code>"true"</code>.
    * 
    * @return   See above.
    */
   public boolean isPrivate()
   {
      return isPrivate;
   }
   
   /**
    * Return <code>true</code> if internal-entry is a function (its {@link #type} is set to
    * {@link Type#FUNCTION}).
    * 
    * @return   See above.
    */
   public boolean isFunction()
   {
      return type == Type.FUNCTION;
   }
   
   /**
    * Return <code>true</code> if this internal-entry has its 
    * <code>in-handle</code> attribute set to <code>"true"</code>.
    * 
    * @return   See above.
    */
   public boolean isInHandle()
   {
      return inHandle;
   }
   
   /**
    * Obtain the original name of the procedure being executed as specified in the 4GL
    * code.
    *
    * @return   The original (unconverted) name.
    */
   public String getLegacyName()
   {
      return pname;
   }
   
   /**
    * Get the qualified OO name (if it applies).
    * 
    * @return   See above.
    */
   public String getQualified()
   {
      return qualified;
   }
   
   /**
    * Get the return's extent (if it applies).
    * 
    * @return   See above.
    */
   public int getExtent()
   {
      return extent;
   }
   
   /**
    * Get the Java {@link #method}.
    * 
    * @return   See above.
    */
   public Method getMethod()
   {
      if (method == null && extprog != null && extprog.ooname == null)
      {
         SourceNameMapper.buildLegacyProgram(extprog);
         
         // in case the Java method is not needed for this entry (is a in-handle or in-super), avoid multiple 
         // lookups.
         extprog = null;
      }
      
      return method;
   }
   
   /**
    * Get the value of the given attribute.  If the attribute is null or
    * is not set, return <code>null</code>.
    * 
    * @param    attr
    *           The attribute's name.
    *           
    * @return   See above.
    */
   public String getAttribute(String attr)
   {
      if (attr == null)
      {
         return null;
      }
      
      attr = attr.toLowerCase();
      
      return attributes.get(attr);
   }
   
   /**
    * Set the value of the given attribute.
    * 
    * @param    name
    *           The attribute's name.
    * @param    value
    *           The attribute's value.
    */
   public void putAttribute(String name, String value)
   {
      attributes.put(name.toLowerCase().intern(), value.intern());
   }
   
   /**
    * Gets the signature of this internal-entry, as returned by the <code>get-signature</code>
    * API of an persistent procedure. If this is the first access, it initializes the 
    * {@link #signature} field.
    * 
    * @return   The 4GL-compatible signature string.
    */
   public String getSignature()
   {
      return signature;
   }
   
   /**
    * Gets the details of the given parameter.
    *
    * @param    idx
    *           0-based parameter index.
    * 
    * @return   The parameter instance or <code>null</code> if no such parameter exists.
    */
   public Parameter getParameter(int idx)
   {
      if (idx < 0 || idx >= parameters.size())
      {
         return null;
      }
      
      return parameters.get(idx);
   }
   
   /**
    * Returns the number of parameters for this internal entry.
    *
    * @return   The size of the parameter list.
    */
   public int getParameterListSize()
   {
      return parameters.size();
   }
   
   /**
    * Returns the list of parameters for this internal entry.
    *
    * @return   The parameter list.
    */
   public List<Parameter> getParameterList()
   {
      // this is safe because the list cannot be modified
      return parameters;
   }
   
   /**
    * Gets the string representation of the parameter modes for this internal-entry.
    * 
    * @return   See above.
    */
   public String getParameterModes()
   {
      return parameterModes;
   }
   
   /**
    * Gets the parameters for this internal entry.
    * 
    * @return   See above.
    */
   public List<Parameter> getParameters()
   {
      return new ArrayList<>(parameters);
   }
   
   /**
    * Set the target Java method for this internal entry to the specified one.
    * 
    * @param    mthd
    *           The target method.
    */
   void setMethod(Method mthd)
   {
      this.method = mthd;
   }
   
   /**
    * Set the external program for this entry.
    * 
    * @param    extprog
    *           The external program for this entry.
    */
   void setExternalProgram(ExternalProgram extprog)
   {
      this.extprog = extprog;
   }
   
   /**
    * Sets the list of parameters for this internal entry.  Changes to the input list will
    * not be reflected in the saved list.
    *
    * @param    parameters
    *           The parameter list.
    */
   protected void setParameterList(List<Parameter> parameters)
   {
      this.parameters = Collections.unmodifiableList(parameters);
   }
   
   /**
    * Initialize this object's internal state. This method must be called after all attributes and
    * parameters have been set, but before any public access to {@link #isSuper()} is possible.
    * This is done within synchronized code in {@link SourceNameMapper}, such that the creation
    * of this object and initialization of this state is atomic from a concurrency perspective.
    */
   protected void initialize()
   {
      isSuper = "true".equalsIgnoreCase(getAttribute("in-super"));
      isPrivate = "true".equalsIgnoreCase(getAttribute("private"));
      inHandle = "true".equalsIgnoreCase(getAttribute("in-handle"));
      
      String sextent = getAttribute("extent");
      if (sextent != null)
      {
         extent = Integer.parseInt(sextent);
      }
      qualified = getAttribute("qualified");
      
      libname = (type == InternalEntry.Type.DLL_ENTRY) ? getAttribute("libname") : null;

      mapTo = (type == Type.FUNCTION) ? getAttribute("map-to") : null;
      
      // must come after attributes
      signature = buildSignature().intern();
      
      if (!parameters.isEmpty())
      {
         StringBuilder sb = new StringBuilder();
         
         for (int i = 0; i < parameters.size(); i++)
         {
            Parameter p = parameters.get(i);
            
            String pmode = p.getMode();
            String type = p.getType();
            char mode = 0;
            
            if ("BUFFER".equalsIgnoreCase(type))
            {
               mode = BUFFER_MODE;
            }
            else if ("INPUT".equalsIgnoreCase(pmode))
            {
               mode = INPUT_MODE;
            }
            else if ("INPUT-OUTPUT".equalsIgnoreCase(pmode))
            {
               mode = INPUT_OUTPUT_MODE;
            }
            else if ("OUTPUT".equalsIgnoreCase(pmode) || "RETURN".equalsIgnoreCase(pmode))
            {
               // the resulting string is used for validation purposes, so we must convert
               // RETURN parms to OUTPUT (although since we defer validation to 
               // NativeInvoker.invoke(), I'm not sure this is actually needed)
               mode = OUTPUT_MODE;
            }
            else
            {
               String err = "Malformed parameter mode " + pmode + " for parameter " + pname;
               throw new IllegalStateException(err);
            }
            
            sb.append(mode);
         }
         
         parameterModes = sb.toString().intern();
      }
   }
   
   /**
    * Build the 4GL-style signature of this instance.
    * 
    * @return   The internal-entry's signature.
    */
   private String buildSignature()
   {
      // The format is: TYPE,return type,param1,param2,param3 where:
      // - TYPE is either PROCEDURE, FUNCTION, EXTERN or DLL-ENTRY
      // - return type is the 4GL name of the return data type
      // - param# is one of: 
      //     INPUT varname TYPE
      //     OUTPUT varname TYPE or INPUT-OUTPUT varname TYPE.
      //   Varname is either the original 4GL variable name or "arg#", if this
      //   is a header definition (i.e. an IN SUPER or IN handle) which had
      //   its parameter set as:
      //   "function f1 returns int (input int) in super."
      
      String type    = this.type.toString();
      String returns = getAttribute("returns");
      String qualified = getAttribute("qualified");
      
      if (qualified != null)
      {
         returns = qualified;
      }
               
      if (returns == null)
      {
         returns = "";
      }
      
      if (isSuper() || isInHandle())
      {
         returns = returns.toUpperCase();
      }
      else
      {
         returns = returns.toLowerCase();
      }
      
      StringBuilder sb = new StringBuilder();
      
      if (isInHandle())
      {
         type = Type.EXTERN.toString();
      }
      
      sb.append(type).append(",");
      sb.append(returns);
      
      if (parameters.size() == 0)
      {
         sb.append(",");
      }
      else
      {
         int idx = 0;
         
         for (Parameter p : parameters)
         {
            idx++;
            
            sb.append(",");
            
            // we can't just use the raw mode string, we must use the version that is meant
            // for the signature string since RETURN is reported by the 4gl as OUTPUT
            String pmode = p.getSignatureMode();
            
            if (pmode.length() > 0)
            {
               sb.append(pmode).append(" ");
            }
            
            if ("BUFFER".equalsIgnoreCase(p.getType()))
            {
               String buffer = p.getAttribute("buffer");
               String forTable = p.getAttribute("for");
               
               sb.append("BUFFER").append(" ");
               sb.append(buffer).append(" ");
               sb.append(forTable);
            }
            else if ("TABLE".equalsIgnoreCase(p.getType()))
            {
               String table = p.getAttribute("table");
               String append = p.getAttribute("append");
               
               sb.append("TABLE").append(" ");
               sb.append(table);
               
               if ("true".equalsIgnoreCase(append))
               {
                  sb.append(" ").append("APPEND");
               }
            }
            else if ("HANDLE".equalsIgnoreCase(p.getType()) && p.getAttribute("TABLE") != null)
            {
               String name = p.getAttribute("pname");
               
               sb.append("TABLE-HANDLE").append(" ");
               sb.append(name);
            }
            else if ("DATASET".equalsIgnoreCase(p.getType()))
            {
               String dataset = p.getAttribute("dataset");
               
               sb.append("DATASET").append(" ");
               sb.append(dataset);
            }
            else if ("HANDLE".equalsIgnoreCase(p.getType()) && p.getAttribute("DATASET") != null)
            {
               String name = p.getAttribute("pname");
               
               sb.append("DATASET-HANDLE").append(" ");
               sb.append(name);
            }
            else
            {
               String pname = p.getLegacyName();
               
               if (pname == null || pname.length() == 0)
               {
                  pname = "arg" + idx;
               }
               
               sb.append(pname).append(" ");

               if ("OBJECT".equalsIgnoreCase(p.getType()) || "JOBJECT".equals(p.getType())) 
               {
                  sb.append(p.getQualified().toUpperCase());
               } 
               else
               {
                  sb.append(p.getType());
               }
               
               // emit extent info
               if (p.isExtent())
               {
                  sb.append(" EXTENT");
                  if (p.isFixedExtent())
                  {
                     sb.append(" ").append(p.getExtent());
                  }
               }
            }
         }
      }
      
      return sb.toString();
   }
}