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 +"]");
         }
      }
   }
}