PostCompilationOpsDriver.java
/*
** Module : PostCompilationOpsDriver.java
** Abstract : class used for operations immediately after the compilation stage.
**
** Copyright (c) 2024-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- ----------------------------------Description-----------------------------------
** 001 AD 20240312 Created new class for additional operations at compile stage, like early
** generation of DMO implementation.
** AD 20240321 Added generation of DMO buffer proxy classes.
** AD 20240430 Moved some misplaced logic from class ProxyFactory to handleDmoBufInterface.
** AD 20240509 Creating the jars at the end of the DMO generation.
** 002 ICP 20240704 Ensure Java 8 compatibility by replacing List.of() with Arrays.asList().
* ICP 20240723 Stopped execution if no valid directories are found.
* ICP 20240805 When the jars are created, write the jar index as well.
** 003 ICP 20250120 Fixed conditions for including DMOs in jars based on filename patterns, added checks for
** path arguments and improved informative logging.
** 004 ICP 20250131 Ensured all pre generated proxies implement ProxyMarker interface.
** Added generation of the reference proxy.
** Updated CacheKey to account for proxyParent and proxyIsHandler.
*/
/*
** 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.compile;
import java.io.*;
import java.lang.reflect.Method;
import java.net.*;
import java.util.*;
import java.util.function.Supplier;
import java.util.logging.*;
import java.util.jar.*;
import org.apache.commons.io.FileUtils;
import com.goldencode.p2j.persist.*;
import com.goldencode.p2j.persist.annotation.*;
import com.goldencode.p2j.persist.orm.*;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.logging.*;
import com.goldencode.proxy.*;
import com.goldencode.proxy.ProxyFactory.CacheKey;
/**
* Entry point used for extra operations at the end of the compilation stage.
* Currently used for:
* Generating at end of compilation stage the implementation classes for DMO interfaces,
* proxy and reference proxy classes for DMO buffer interfaces.
*/
public class PostCompilationOpsDriver
{
/** Logger */
private static final CentralLogger log = CentralLogger.get(PostCompilationOpsDriver.class);
/** Do we log WARNING-level events ? */
private static final boolean LOG_WARNING = log.isLoggable(Level.WARNING);
/** Do we log FINE-level events ? */
private static final boolean LOG_FINE = log.isLoggable(Level.FINE);
/**
* The directories where all target .class files are located.
* Given at command line as one string with paths separated by ':'.
* Paths should be relative to the current working directory.
*/
private static String[] classesDirectories;
/** The directories in which all the DMO implementation classes will be saved. */
private static String[] dmoImplClassesDirectories;
/** The directories in which all the DMO implementation classes will be saved. */
private static String[] dmoBufImplClassesDirectories;
/** A relative path from a {@link #classesDirectories} path to the folder containing the DMO classes. */
private static String dmosDirectory;
/** A set of folders to skip processing. */
private static Set<String> dmoPathsBlacklist;
/** Character used to separate paths in the blacklist given as argument. */
private static String dmoBlacklistSeparator = ":";
/** A URL class loader used for loading the DMO interface classes from any provided path. */
private static URLClassLoader classLoader;
/** Cache mapping a string representation of a {@link CacheKey} to the name of a new proxy class.*/
private static Map<String, String> proxyCache = new HashMap<>();
/** The file/path where the {@link #proxyCache} will be saved on disk. */
private static File proxyCacheFile;
/** Path to the jar containing the DMO implementations. */
private static String dmoImplsJarPath;
/** Path to the jar containing the DMO buffer proxy classes. */
private static String dmoBufProxiesJarPath;
/**
* Main method of entry point where post compilation operations are started.
* These operations include:
* Generating implementation classes of existing DMO interfaces and saving them to disk.
*
* @param args
* List of command line arguments. The arguments are as follow:
* args[0] - folders containing .class files, separated by :
* example: ${build.home}/classes/:${build.home}/classes2/:${build.home}/classes3/
* args[1] - relative path from any of the folders in args[0] to a folder containing DMOs
* args[2] - blacklist paths, won't process these schema folders, separated by :
* args[3] - where to save all the dmo implementations
* args[4] - where to save all the dmo buf proxies
* args[5] - where to save the map file that helps associate a Proxy class file name to the
* interfaces/classes names that were used to generate it.
* args[6] - where the jar with the dmo implementations will be saved
* args[7] - where the jar with the dmo buffer proxies will be saved
*/
public static void main(String[] args)
{
classesDirectories = args[0].split(":");
dmosDirectory = args[1];
dmoPathsBlacklist = new HashSet<>(Arrays.asList(args[2].split(dmoBlacklistSeparator)));
dmoImplClassesDirectories = args[3].split(":");
dmoBufImplClassesDirectories = args[4].split(":");
proxyCacheFile = new File(args[5]);
if (proxyCacheFile.exists())
{
proxyCacheFile.delete();
}
dmoImplsJarPath = args[6];
dmoBufProxiesJarPath = args[7];
createImplementations();
}
/**
* Method responsible for searching for all DMO interfaces and DMO buffer interfaces
* in the provided directory and attempt to create an implementation/proxy class
* and save that class to disk.
*/
private static void createImplementations()
{
File schemaDir;
File dmosRootDir;
String[] schemaDirectories;
List<String> fileNames;
String fileBinaryName;
boolean processingDone = false;
// Ensure dmosDirectory does not begin with a "/", but ends with "/"
if (dmosDirectory.startsWith("/"))
{
dmosDirectory = dmosDirectory.substring(1);
}
if (!dmosDirectory.endsWith("/"))
{
dmosDirectory += "/";
}
for (int k = 0; k < classesDirectories.length; k++)
{
// Ensure classesDirectories[k] ends with a "/"
if (!classesDirectories[k].endsWith("/"))
{
classesDirectories[k] += "/";
}
dmosRootDir = new File(classesDirectories[k] + dmosDirectory);
if (!dmosRootDir.exists())
{
continue;
}
schemaDirectories = dmosRootDir.list();
fileBinaryName = null;
try
{
classLoader = URLClassLoader.newInstance(
new URL[] {new File(classesDirectories[k]).toURI().toURL()},
PostCompilationOpsDriver.class.getClassLoader() );
}
catch (MalformedURLException e)
{
if (LOG_WARNING)
{
log.log(Level.WARNING, "Failed to initialize URLClassLoader with path: "
+ classesDirectories[k]);
}
else if (LOG_FINE)
{
log.log(Level.FINE, "Failed to initialize URLClassLoader with path: "
+ classesDirectories[k], e);
}
return;
}
for (int i = 0; i < schemaDirectories.length; i++)
{
schemaDir = new File(dmosRootDir.getPath() + "/" + schemaDirectories[i]);
if (dmoPathsBlacklist.contains(schemaDir.getPath()) ||
!schemaDir.exists() ||
!schemaDir.isDirectory())
{
continue;
}
fileNames = new ArrayList<>(Arrays.asList(schemaDir.list()));
fileNames.removeIf((fileName) -> !fileName.endsWith(".class"));
log.info("Processing files in " + schemaDir.getAbsolutePath() +"..");
if (fileNames.isEmpty())
{
continue;
}
processingDone = true;
int dmoInterfacesProcessed = 0;
int dmoBufferInterfacesProcessed = 0;
for (int j = 0; j < fileNames.size(); j++)
{
fileBinaryName = dmosDirectory + schemaDirectories[i] + "/" + fileNames.get(j);
fileBinaryName = fileBinaryName.substring(0, fileBinaryName.length() - ".class".length())
.replace('/', '.');
if (isDmoBufInterface(fileNames.get(j), schemaDirectories[i]))
{
if (handleDmoBufInterface(fileBinaryName, dmoBufImplClassesDirectories[k]))
{
//there are 2 proxies generated for each DMO buffer interface
dmoBufferInterfacesProcessed = dmoBufferInterfacesProcessed + 2;
}
continue;
}
if (isDmoInterface(fileNames.get(j), schemaDirectories[i]))
{
if (handleDmoInterface(fileBinaryName, dmoImplClassesDirectories[k]))
{
dmoInterfacesProcessed++;
}
continue;
}
}
log.info("Generated " +
dmoInterfacesProcessed +
" DMO Interfaces and " +
dmoBufferInterfacesProcessed +
" DMO Buffer Interfaces.");
}
}
if (processingDone)
{
writeProxyCacheToDisk();
createJar(dmoImplsJarPath, dmoImplClassesDirectories);
createJar(dmoBufProxiesJarPath, dmoBufImplClassesDirectories);
}
else
{
log.severe("Directories are empty, failed to create JARs.");
}
}
/**
* Method used to create a jar from a given list of directories.
* The directories themselves will not be part of each jar entries path/name.
* If a jar with this path already exists, it will first be deleted.
* The classes directories will also be deleted when the files within are
* added to the jar.
*
* @param jarPath
* The path of the new jar.
* @param classesFolders
* The folders to be jarred.
*/
private static void createJar(String jarPath, String[] classesFolders)
{
File jarFile = new File(jarPath);
if (jarFile.exists())
{
jarFile.delete();
}
jarFile.getParentFile().mkdirs();
try (FileOutputStream fos = new FileOutputStream(jarFile);
JarOutputStream jos = new JarOutputStream(fos))
{
Set<String> packages = new HashSet<>();
for (int i = 0; i < classesFolders.length; i++)
{
File file = new File(classesFolders[i]);
File[] files = file.listFiles();
if (files != null)
{
for (int j = 0; j < files.length; j++)
{
createJarHelper(files[j], jos, files[j].getParent().length() + 1, packages);
}
FileUtils.deleteDirectory(file);
}
}
writeJarIndex(jos, packages);
}
catch (Exception e)
{
log.severe("Failed to create JAR: " + jarPath, e);
}
}
/**
* Writes the jar index to the jar file's META-INF directory.
*
* @param jos
* The JarOutputStream to write the index to.
* @param packages
* The set of package names to include in the index.
*
* @throws IOException
* Exception that can be thrown by the JarOutputStream object, not handled here.
*/
private static void writeJarIndex(JarOutputStream jos, Set<String> packages)
throws IOException
{
JarEntry indexEntry = new JarEntry("META-INF/INDEX.LIST");
jos.putNextEntry(indexEntry);
jos.write("JarIndex-Version: 1.0\n\n".getBytes());
jos.write("./\n".getBytes());
for (String pkg : packages)
{
jos.write((pkg + "\n").getBytes());
}
jos.closeEntry();
}
/**
* Helper method for creating a jar. Receives the file/directory to be added to the jar,
* and the stream used to write to the jar.
* Calls itself recursively for every directory in the source File, if that is
* a directory. Otherwise, it adds the source File to the jar.
*
* @param source
* The class file or directory containing class files that will be added to the jar.
* @param target
* The stream used to add files to the jar.
* @param nameSubstringIndex
* How much of the sources path name to skip when adding the new entry to the jar.
* @param packages
* The set to collect package names.
*
* @throws IOException
* Exception that can be thrown by the JarOutputStream object, not handled here.
*/
private static void createJarHelper(File source, JarOutputStream target, Integer nameSubstringIndex, Set<String> packages)
throws IOException
{
String name = source.getPath().replace("\\", "/").substring(nameSubstringIndex);
if (source.isDirectory())
{
if (!name.endsWith("/"))
{
name += "/";
}
JarEntry entry = new JarEntry(name);
entry.setTime(source.lastModified());
target.putNextEntry(entry);
target.closeEntry();
File[] files = source.listFiles();
for (int i = 0; i < files.length; i++)
{
createJarHelper(files[i], target, nameSubstringIndex, packages);
}
}
else
{
JarEntry entry = new JarEntry(name);
entry.setTime(source.lastModified());
target.putNextEntry(entry);
try (FileInputStream fis = new FileInputStream(source);
BufferedInputStream bis = new BufferedInputStream(fis))
{
byte[] buffer = new byte[1024];
while (true)
{
int count = bis.read(buffer);
if (count == -1)
{
break;
}
target.write(buffer, 0, count);
}
target.closeEntry();
}
String packageName = name.substring(0, name.lastIndexOf('/')).replace('/', '.');
packages.add(packageName);
}
}
/**
* This method is responsible for trying to create a proxy class and a reference proxy class for a DMO
* buffer interface.
* The steps include attempting to load the DMO buffer interface and the DMO interface and for each of
* the proxies generating the proxy assembler, adding the new proxy class data to the cache and
* writing the code to a .class file on disk.
* All generated proxies explicitly implement {@link ProxyMarker} to allow identification
* at runtime even when loaded from a pre-generated JAR.
*
* @param dmoBufBinaryName
* The name of the DMO buffer interface, with dot separators between packages.
* @param classDestination
* The folder where the .class file and it's packages will be created.
*/
private static boolean handleDmoBufInterface(String dmoBufBinaryName, String classDestination)
{
Class<? extends DataModelObject> dmoIface;
Class<?> dmoBufIface;
String dmoIfaceName = dmoBufBinaryName.substring(0, dmoBufBinaryName.length() - "$Buf".length());
try
{
dmoBufIface = (Class<?>) classLoader.loadClass(dmoBufBinaryName);
dmoIface = (Class<? extends DataModelObject>) classLoader.loadClass(dmoIfaceName);
}
catch (ClassNotFoundException e)
{
if (LOG_FINE)
{
log.log(Level.FINE, "Failed to load " + dmoBufBinaryName, e);
}
else if (LOG_WARNING)
{
log.log(Level.WARNING, "Failed to load " + dmoBufBinaryName +
" (set debug level to FINE for stacktrace)");
}
return false;
}
try
{
Class<?>[] interfaces = Temporary.class.isAssignableFrom(dmoBufIface)
? new Class[] { ProxyMarker.class,
dmoBufIface,
BufferReference.class,
TempTableRecord.class}
: new Class[] { ProxyMarker.class, dmoBufIface, BufferReference.class };
Arrays.sort(interfaces, Comparator.comparing(Class::getName));
// proxy generation
Class<?> parent = BufferImpl.class;
boolean proxyIsHandler = false;
boolean declaredOnly = false;
boolean proxyParent = false;
Supplier<ProxyAssemblerPlugin> plugin = () -> new DmoProxyPlugin(dmoBufIface, dmoIface, true);
Method[] doNotProxy = RecordBuffer.getDoNotProxy();
ProxyFactory.CacheKey proxyKey = new CacheKey(parent,
proxyParent,
proxyIsHandler,
interfaces,
declaredOnly,
doNotProxy);
ProxyAssembler proxyAssembler = ProxyFactory.getProxyAssembler(parent,
proxyParent,
interfaces,
declaredOnly,
doNotProxy,
proxyIsHandler,
plugin);
if (proxyAssembler == null)
{
return false;
}
String proxyName = proxyAssembler.getProxyName();
byte[] code = proxyAssembler.assembleClass();
proxyCache.put(proxyKey.toString(), proxyName.replace("/", "."));
writeClassToDisk(proxyName, code, classDestination);
// reference proxy generation
proxyParent = true;
Method[] doNotProxyRes = new Method[] { HandleResource.class.getDeclaredMethod("processResource") };
ProxyFactory.CacheKey proxyKeyRes = new CacheKey(parent,
proxyParent,
proxyIsHandler,
interfaces,
declaredOnly,
doNotProxyRes);
ProxyAssembler proxyAssemblerRes = ProxyFactory.getProxyAssembler(parent,
proxyParent,
interfaces,
declaredOnly,
doNotProxyRes,
proxyIsHandler,
plugin);
if (proxyAssemblerRes == null)
{
return false;
}
String proxyNameRes = proxyAssemblerRes.getProxyName();
byte[] codeRes = proxyAssemblerRes.assembleClass();
proxyCache.put(proxyKeyRes.toString(), proxyNameRes.replace("/", "."));
writeClassToDisk(proxyNameRes, codeRes, classDestination);
}
catch (Exception e)
{
if (LOG_WARNING)
{
log.warning("Unexpected exception encountered at creating proxies for dmo buf interface: " +
dmoBufBinaryName + ".", e);
}
else if (LOG_FINE)
{
log.fine("Unexpected exception encountered at creating proxies for dmo buf interface: " +
dmoBufBinaryName + ".");
}
return false;
}
return true;
}
/**
* This method is responsible for attempting to load a DMO interface class with the
* given binary name (package name, with dot separator), generate for it
* an implementation class object and save the new class to a file on disk.
*
* @param dmoBinaryName
* The name of a DMO buffer interface, with dot separators between packages.
* @param classDestination
* The folder where the .class file and it's packages will be created.
*/
private static boolean handleDmoInterface(String dmoBinaryName, String classDestination)
{
DmoClass dmoImpl;
Class<? extends DataModelObject> dmoIface;
try
{
dmoIface = (Class<? extends DataModelObject>) classLoader.loadClass(dmoBinaryName);
dmoImpl = generateImplementationDmo(dmoIface);
writeClassToDisk(dmoImpl.getInternalName(), dmoImpl.getBytecode(), classDestination);
}
catch (ClassNotFoundException | NoClassDefFoundError e)
{
if (LOG_FINE)
{
log.log(Level.FINE, "Failed to load " + dmoBinaryName, e);
}
else if (LOG_WARNING)
{
log.log(Level.WARNING, "Failed to load " + dmoBinaryName +
" (set debug level to FINE for stacktrace)");
}
return false;
}
catch (Exception e)
{
if (LOG_FINE)
{
log.log(Level.FINE, "Failed to assemble implementation for " + dmoBinaryName, e);
}
else if (LOG_WARNING)
{
log.log(Level.WARNING, "Failed to load " + dmoBinaryName +
" (set debug level to FINE for stacktrace)");
}
return false;
}
return true;
}
/**
* Check whether the name given respects a minimum of conditions to be
* considered the name of a DMO buffer interface.
*
* @param fileName
* The name of a class file, with the extension.
* @param schemaDirectory
* The name of the schema directory that contains this class file.
*
* @return True if it can be a DMO buffer interface name. False otherwise.
*/
private static boolean isDmoBufInterface(String fileName, String schemaDirectory)
{
boolean tempSchema = schemaDirectory.equals("_temp");
String regex = ".*_\\d+_1\\$Buf\\.class$";
return !fileName.contains("__Impl__") &&
fileName.contains("$") &&
(!tempSchema ||
(tempSchema && fileName.matches(regex)));
}
/**
* Check whether the name given respects a minimum of conditions to be
* considered the name of a DMO interface.
*
* @param fileName
* The name of a class file, with the extension.
* @param schemaDirectory
* The name of the schema directory that contains this class file.
*
* @return True if it can be a DMO interface name. False otherwise.
*/
private static boolean isDmoInterface(String fileName, String schemaDirectory)
{
boolean tempSchema = schemaDirectory.equals("_temp");
String regex = ".*_\\d+_1\\.class$";
return !fileName.contains("__Impl__") &&
!fileName.contains("$") &&
(!tempSchema ||
(tempSchema && fileName.matches(regex)));
}
/**
* This method is responsible for setting up objects needed to generate a DMO implementation class
* for a DMO interface provided, and use {@link DmoClass} to create and provide the DMO class object.
*
* @param dmoIface
* The class of a DMO interface for which we generate an implementation class.
*
* @return An object representing the newly generated DMO implementation class.
*/
private static DmoClass generateImplementationDmo(Class<? extends DataModelObject> dmoIface)
{
if (dmoIface == null)
{
return null;
}
Class<? extends DataModelObject> dmoImplInterface = dmoIface;
// get the interface that contains the actual DMO annotations
dmoIface = DmoMetadataManager.getAnnotatedInterface(dmoIface);
if (dmoIface == null)
{
// no DMO interface detected.
throw new IllegalArgumentException(
"The " + dmoIface + " is not implementing DataModelObject.");
}
Table tableAnn = dmoIface.getAnnotation(Table.class);
if (tableAnn == null)
{
throw new IllegalArgumentException(
"@Table annotation not detected in DataModelObject (" + dmoImplInterface + ").");
}
DmoMeta dmoMeta = new DmoMeta(dmoImplInterface, dmoIface, tableAnn);
return DmoClass.assembleAndRetrieveImplementation(dmoMeta);
}
/**
* This method is responsible for writing to disk the {@link #proxyCache} map object,
* meant to be retrieved by {@link ProxyFactory} at server runtime.
*/
private static void writeProxyCacheToDisk()
{
proxyCacheFile.getParentFile().mkdirs();
try (FileOutputStream fos = new FileOutputStream(proxyCacheFile);
ObjectOutputStream oos = new ObjectOutputStream(fos);)
{
oos.writeObject(proxyCache);
}
catch (IOException e)
{
if (LOG_FINE)
{
log.log(Level.FINE, "Failed to write proxy cache to file.", e);
}
else if (LOG_WARNING)
{
log.log(Level.WARNING, "Failed to write proxy cache to file." +
" (set debug level to FINE for stacktrace)");
}
}
}
/**
* This method is used to write to disk the bytecode of a class, with all necessary information
* required given as parameter.
*
* @param internalName
* The internal name of the class to write to file, containing the class name and package
* with slash separators.
* @param code
* The bytecode of the class that needs to be written to file.
* @param destinationFolder
* The folder path where the new .class file will be saved.
* Additional directories will be created for the packages that appear in the internalName argument.
*/
private static void writeClassToDisk(String internalName, byte[] code, String destinationFolder)
{
String fileName = destinationFolder + internalName + ".class";
File file = new File(fileName);
file.getParentFile().mkdirs();
try (FileOutputStream fos = new FileOutputStream(file))
{
fos.write(code);
}
catch (IOException e)
{
if (LOG_WARNING)
{
log.warning("Failed to write class binary for [" + internalName +"]");
}
}
}
}