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