RcodeInfoOps.java

/*
** Module   : RcodeInfoOps.java
** Abstract : Utility for working with RCODE-INFO system handle.
**
** Copyright (c) 2017-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- -------------------------------------- Description --------------------------------------
** 001 SVL 20170126 Created initial version.
** 002 EVL 20190426 Adding CODEPAGE attribute getter.
** 003 CA  20190530 Added IS-CLASS stub.
** 004 OM  20210318 Added *read-only* setter for CODEPAGE.
**     CA  20210527 Added runtime support.
**     VVT 20221003 CommonHandle.getResourceType() method renamed to resourceType() to prevent conflicts
**                  with namesakes in DMO. See #6694.
**     CA  20220426 Added TABLE-LIST support.
**     VVT 20220913 CommonHandle.getResourceType() method renamed to resourceType() to prevent conflicts
**                  with namesakes in DMO. See #6694.
**     IAS 20221129 Added stubs for missed RCODE-INFO attributes.
** 005 DMM 20250129 Implemented getMD5Val().
*/

/*
** 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.nio.file.*;
import java.io.*;
import java.net.*;
import java.security.*;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.Enumeration;
import java.util.logging.Level;

import com.goldencode.p2j.oo.lang.*;
import com.goldencode.p2j.security.*;
import com.goldencode.proxy.*;
import com.goldencode.p2j.util.logging.*;
import com.goldencode.p2j.classloader.MultiClassLoader;

/**
 * Utility for working with RCODE-INFO system handle that lets you read information about r-code files.
 */
public class RcodeInfoOps
{
   /** Logger*/
   private static final CentralLogger LOG = CentralLogger.get(RcodeInfoOps.class.getName());

   /** Stores context-local state variables. */
   private static ContextLocal<WorkArea> local = new ContextLocal<WorkArea>()
   {
      /** Retrieve the initial value of this context local variable. */
      @Override
      protected WorkArea initialValue()
      {
         return new WorkArea();
      }
   };

   /**
    * Get the RCODE-INFO system handle.
    *
    * @return RCODE-INFO system handle.
    */
   public static handle asHandle()
   {
      WorkArea wa = local.get();
      
      Class[] classes = { RcodeInfoOps.class };
      RcodeInfo proxy = StaticProxy.obtain(RcodeInfo.class, classes);
      
      if (wa.id == null)
      {
         wa.id = handle.resourceId(proxy);
      }
      
      return new handle(proxy);
   }

   /**
    * Get the name of the target r-code file.
    *
    * @return the name of the target r-code file.
    */
   public static character getFileName()
   {
      WorkArea wa = local.get();
      return wa.info == null ? new character() : new character(wa.info.fileName);
   }

   /**
    * Set the name of the target r-code file.
    *
    * @param fileName
    *        The name of the target r-code file.
    */
   public static void initFileInfo(character fileName)
   {
      WorkArea wa = local.get();
      
      if (fileName == null || fileName.isUnknown())
      {
         wa.info = null;
         return;
      }
      
      initFileInfo(fileName.toStringMessage());
   }

   /**
    * Set the name of the target r-code file.
    *
    * @param fileName
    *        The name of the target r-code file.
    */
   public static void initFileInfo(String fileName)
   {
      WorkArea wa = local.get();
      if (fileName == null)
      {
         wa.info = null;
         return;
      }
      
      String javaClass = SourceNameMapper.convertNameToClass(fileName);
      
      wa.info = new LegacyProgramInfo();
      wa.info.fileName = fileName;
      
      if (javaClass != null)
      {
         try
         {
            // the class is loaded in the JVM here, but this will not initialize it in terms of legacy class
            // support - and this is how 4GL works, no static constructors are called. 
            Class<?> cls = Class.forName(javaClass);
   
            wa.info.initialized = true;
            wa.info.isClass = resolveIsClass(cls);
            wa.info.dbReferences = resolveDbReferences(cls);
            wa.info.tableList = resolveTableList(cls);
            wa.info.md5Value = null;
         }
         catch (ClassNotFoundException e)
         {
            // something happened... TODO: log it?
         }
      }
   }

   /**
    * Get the list of the databases, referenced by the target r-code file.
    *
    * @return comma-separated list of the databases (in the form of logical database names or aliases),
    *         referenced by the target r-code file.
    */
   public static character getDbReferences()
   {
      WorkArea wa = local.get();
      return wa.info == null || !wa.info.initialized ? new character() : new character(wa.info.dbReferences);
   }

   /**
    * Get the list of the tables, referenced by the r-code file.
    *
    * @return   comma-separated list of the tables (in the form of <code>schema.table</code>),
    *           referenced by the r-code file.
    */
   public static character getTableList()
   {
      WorkArea wa = local.get();
      return wa.info == null || !wa.info.initialized ? new character() : new character(wa.info.tableList);
   }

   /**
    * Definition of the IS-CLASS method.
    * 
    * @return    <code>true</code> if the current program is a NEW-ly created object.
    */
   public static logical isClass()
   {
      WorkArea wa = local.get();
      return wa.info == null || !wa.info.initialized ? new logical() : new logical(wa.info.isClass);
   }
   
   /**
    * Get the CODEPAGE attribute referenced by the target r-code file.
    *
    * @return The current CODEPAGE attribute for handle referenced by the target r-code file.
    */
   public static character getCodePage()
   {
      return I18nOps.getCodePage();
   }
   
   /**
    * Get a comma-separated list of the CRC value for each table referenced in the r-code file
    * @return a comma-separated list of the CRC value for each table referenced in the r-code file
    */
   @LegacyAttribute(name = "TABLE-CRC-LIST")
   public static character getTableCrcList()
   {
      // TODO: implement
      return new character();
   }

   /**
    * Get the cyclic redundancy check (CRC) value for an r-code file.
    * @return the cyclic redundancy check (CRC) value for an r-code file/
    */
   @LegacyAttribute(name = "CRC-VALUE")
   public static character getCrcVal()
   {
      // TODO: implement
      return new character();
   }

   /**
    * Get the type of display for an r-code file. 
    * @return the type of display for an r-code file.
    */
   @LegacyAttribute(name = "DISPLAY-TYPE")
   public static character getRCodeDisplayType()
   {
      // TODO: implement
      return new character();
   }
   
   /**
    * Check whether the r-code was compiled with the -nocolon startup parameter in effect.
    * 
    * @return <code>true</code> if the r-code was compiled with the -nocolon startup parameter in effect.
    */
   @LegacyAttribute(name = "LABELS-HAVE-COLONS")
   public static logical isLabelsHaveColons()
   {
      // TODO: implement
      return new logical();
   }
   
   /**
    * Get a comma-separated list of all languages compiled into the r-code file. 
    * 
    * @return a comma-separated list of all languages compiled into the r-code file.
    */
   @LegacyAttribute(name = "LANGUAGES")
   public static character getLanuages()
   {
      // TODO: implement
      return new character();
   }
   
   /**
    * Get the MD5 value stored in an r-code file.
    * By design, the calculated MD5 hash is not compatible with that calculated by OpenEdge
    * because in OE the hash is calculated from r-code and in FWD the hash is calculated from
    * the converted .class file.
    * 
    * @return the MD5 value stored in an r-code file as a 32 character hexadecimal number.
    */
   @LegacyAttribute(name = "MD5-VALUE")
   public static character getMD5Val()
   {
      // Compute md5 value the first time it is read, in rest of cases just get it
      WorkArea wa = local.get();
      if (wa.info != null && wa.info.initialized && wa.info.md5Value == null)
      {
         String javaClass = SourceNameMapper.convertNameToClass(wa.info.fileName);
         wa.info.md5Value = resolveMd5Value(javaClass);
      }

      return wa.info == null || !wa.info.initialized || wa.info.md5Value == ""
                        ? new character() : new character(wa.info.md5Value);
   }

   /**
    * Check if UNDO, THROW has been set as the default error handling directive at either the 
    * routine level or the block level
    * 
    * @return "ROUTINE-LEVEL", or "BLOCK-LEVEL" to indicate which blocks are affected by UNDO, 
    *         THROW error handling. It returns the empty string ("") when default error handling 
    *         is in effect
    */
   @LegacyAttribute(name = "UNDO-THROW-SCOPE")
   public static character getUndoThrowScope()
   {
      // TODO: implement
      return new character();
   }

   /**
    * Attempt to set the CODEPAGE attribute referenced by the target r-code file. This method always throws
    * error 4052 because this attribute is readonly.
    *
    * @param   cp
    *          The current CODEPAGE to be set.
    */
   public static void setCodePage(character cp)
   {
      ErrorManager.recordOrThrowError(4052, "CODEPAGE", "settable", "PSEUDO-WIDGET");
      // **<attribute> is not a <settable/queryable> attribute for <widget id>.
   }
   
   /**
    * API needed to implement read-only attribute assignment (a 4GL "feature").
    * 
    * @param    attribute
    *           The attribute's name.
    *           
    * @see      handle#readOnlyError(handle, String)
    */
   public static void readOnlyError(String attribute)
   {
      handle.readOnlyError(asHandle(), attribute);
   }
   
   /**
    * API needed to implement read-only attribute assignment (a 4GL "feature").
    * 
    * @param    attribute
    *           The attribute's name.
    * @param    expr
    *           The value which is attempted to be assigned to the read-only attribute. 
    *
    * @see      handle#readOnlyError(handle, String, Object)
    */
   public static void readOnlyError(String attribute, Object expr)
   {
      handle.readOnlyError(asHandle(), attribute);
   }

   /**
    * Get the type of its associated handle.
    * 
    * @return   Always return the PSEUDO-WIDGET value.
    */
   public static character resourceType()
   {
      return new character(LegacyResource.PSEUDO_WIDGET);
   }

   /**
    * Implementation for the {@link WrappedResource#valid()} API.
    * 
    * @return   Always true.
    */
   public static boolean valid()
   {
      return true;
   }
   
   /**
    * Get this resource's ID.
    * 
    * @return   The resource's ID.
    */
   public static Long id()
   {
      WorkArea wa = local.get();
      
      return wa.id;
   }
   
   /**
    * Set this resource's ID.
    * <p>
    * This is a no-op for system handles.
    *  
    * @param    id
    *           The resource's ID.
    */
   public static void id(long id)
   {
      // no-op
   }

   /**
    * Implementation for the {@link WrappedResource#unknown()} API.
    * 
    * @return   Always false.
    */
   public static boolean unknown()
   {
      return false;
   }

   /**
    * Check if the given Java class is compatible with legacy, converted classes - in other words, if it
    * implements the {@link _BaseObject_} interface.
    * 
    * @param    cls
    *           The type to check.
    *           
    * @return   See above.
    */
   static boolean resolveIsClass(Class<?> cls)
   {
      return _BaseObject_.class.isAssignableFrom(cls);
   }
   
   /**
    * Read the {@link DatabaseReferences} annotation from the Java class and get the list of the logical 
    * database names or aliases.  If the annotation does not exist, return the empty string.
    *  
    * @param    cls
    *           The Java class for a converted external program or legacy class.
    *           
    * @return   See above.
    */
   static String resolveDbReferences(Class<?> cls)
   {
      // TODO: for web service or proxy procedures it must return unknown.  How to distinguish them? 

      DatabaseReferences dbRefs = cls.getAnnotation(DatabaseReferences.class);
      if (dbRefs == null)
      {
         return "";
      }
      
      String res = "";
      for (String alias : dbRefs.aliases())
      {
         res = res + (res.isEmpty() ? "" : ",") + alias;
      }
      
      return res;
   }
   
   /**
    * Read the {@link DatabaseReferences} annotation from the Java class and get the list of tables.  
    * If the annotation does not exist, return the empty string.
    *  
    * @param    cls
    *           The Java class for a converted external program or legacy class.
    *           
    * @return   See above.
    */
   static String resolveTableList(Class<?> cls)
   {
      // TODO: for web service or proxy procedures it must return unknown.  How to distinguish them? 

      DatabaseReferences dbRefs = cls.getAnnotation(DatabaseReferences.class);
      if (dbRefs == null)
      {
         return "";
      }
      
      String res = "";
      for (String alias : dbRefs.tables())
      {
         res = res + (res.isEmpty() ? "" : ",") + alias;
      }
      
      return res;
   }

   /**
    * Calculates md5 value for specific .class file.
    *
    * @param    javaClass
    *           The qualified Java class name.
    *
    * @return   A string that represents a 32 character hexadecimal value computed based on
    *           .class file content (inclusive inner classes)
    *           or an empty string in case there are problems in reading.
    */
   static String resolveMd5Value(String javaClass)
   {
      String md5Value = "";

      // Get the path for compiled class from deploy/lib/<proj_name.jar>.
      String file = new String(javaClass.replace(".", "/") + ".class");
      URL url = MultiClassLoader.getClassLoader().getResource(file);
      String filePath = url.toString();

      // Find .jar file absolute path and directory path related to .jar file
      String jarPath = filePath.split(".jar!")[0].replace("jar:file:", "") + ".jar";
      String directoryPath = file.substring(0, file.lastIndexOf("/"));

      // Build the prefix for inner classes paths
      String prefix = file.substring(0, file.length() - ".class".length()) + "$";
      try
      {
         // Read the main .class file content
         MessageDigest messageDigest = MessageDigest.getInstance("MD5");
         readFile(url, messageDigest);

         // Read the content of inner classes
         JarFile jarFile = new JarFile(jarPath);
         Enumeration<JarEntry> entries = jarFile.entries();
         while (entries.hasMoreElements())
         {
            JarEntry entry = entries.nextElement();
            if (entry.getName().startsWith(prefix) && !entry.isDirectory())
            {
               url = MultiClassLoader.getClassLoader().getResource(entry.getName());
               readFile(url, messageDigest);
            }
         }

         // Compute md5 value
         byte[] fileContentDigest = messageDigest.digest();

         // Convert the string to hexadecimal string
         StringBuilder hexadecimalValue = new StringBuilder();
         for (byte b : fileContentDigest)
         {
            hexadecimalValue.append(String.format("%02x", b));
         }
         md5Value = hexadecimalValue.toString();
      }
      catch (Exception e)
      {
         if (LOG.isLoggable(Level.WARNING))
         {
            LOG.warning("Problem while trying to calculate md5 value for file:" + url, e);
         }
      }

      return md5Value;
   }

   /**
    * Reads the content of a file using a stream. It is used in resolveMd5Value method.
    *
    * @param    url
    *           An URL instance representing the path to a .class file.
    * @param    messageDigest
    *           A MessageDigest instance used to compute md5 value based on all relevant
    *           .class files.
    */
   private static void readFile(URL url, MessageDigest messageDigest)
   {
      try (InputStream fileInputStream = url.openStream())
      {
         byte[] buffer = new byte[1024];
         int length;
         while ((length = fileInputStream.read(buffer)) != -1)
         {
            messageDigest.update(buffer, 0, length);
         }
      }
      catch (Exception e)
      {
         if (LOG.isLoggable(Level.WARNING))
         {
            LOG.warning("Problem while trying to calculate md5 value for file:" + url, e);
         }
      }
   }

   /** 
    * Stores global data relating to the state of the current context.
    */
   private static class WorkArea
   {  
      /** The resource's ID. */
      private Long id = null;
      
      /** File information for the current file being examined. */ 
      private LegacyProgramInfo info = null; 
   }
   
   /**
    * Details about the r-code information for a legacy program or class.
    */
   private static class LegacyProgramInfo
   {
      /** The filename used to initialize the RCODE-INFO data structure. */
      public String fileName;
      
      /** The list of database references - logical database names or aliases. */
      public String dbReferences;
      
      /** The list of table references. */
      public String tableList;

      /** Md5 value for compiled file.*/
      public String md5Value;
      
      /** Flag indicating if this is for a legacy class. */
      public boolean isClass;
      
      /** Flag indicating if the given {@link #fileName} was resolved as a legacy program or class. */
      public boolean initialized = false;
   }
}