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