JarUtil.java

/*
 ** Module   : JarUtil.java
 ** Abstract : Utility methods for jar files.
 **
 ** Copyright (c) 2024, Golden Code Development Corporation.
 **
 ** -#- -I- --Date-- --------------------------------Description----------------------------------
 ** 001 GBB 20240401 Initial version. Methods moved here from MultiClassLoader and Utils for reusability.
**                   resolveClasspathJars() to return List instead of Set.
**  002 ICP 20240705 Fixed method of obtaining the class loader.
 */
/*
 ** 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 com.goldencode.p2j.classloader.*;
import com.goldencode.p2j.util.logging.*;

import java.io.*;
import java.net.*;
import java.util.*;
import java.util.function.*;
import java.util.logging.*;

/** Utility methods for jar files. */
public class JarUtil
{
   /**
    * Search the resource jar files for the resource with the given filename.
    * <p>
    * The algorithm first attempts to find an exact match for the filename. If not found, it
    * applies the PROPATH to the filename, first prepending the package root to the name.
    * 
    * @param   filename
    *          Filename of the resource, optionally including a path.
    * @param   searchFn
    *          The search function for the resource with the given filename
    * @param   propathSearchFn
    *          The propath search function for the resource with the given filename found in PROPATH
    * @param   propathSupplier
    *          The propath supplier that list PROPATH entries.
    * 
    * @return  A {@code JarResult} object, if the search succeeds, else {@code null}.
    */
   public static JarResource searchResourceJars(
            CentralLogger logger,
            String filename,
            Function<JarClassLoader, Function<String, String>> searchFn,
            Function<JarClassLoader, Function<String, String>> propathSearchFn,
            Supplier<List<String>> propathSupplier)
   {
      final char WIN_SEP = '\\';
      final String JAR_SEP_STR = "/";
      final char JAR_SEP = '/';
      
      // the first thing to do working with jar base filename is replace all Windows separators
      filename = filename.replace(WIN_SEP, JAR_SEP);
      
      // resource name as found in jar file
      String resName = null;
      
      // class loader for jar file in which resource is found
      JarClassLoader jarClassLoader = null;
      
      // get class loader for application jar
      ClassLoader cl = MultiClassLoader.getClassLoader();
      if (cl != null)
      {
         // first clean up initial string
         
         // possible current directory symbol
         int len = filename.length();
         if (len > 0 && filename.charAt(0) == '.')
         {
            filename = filename.substring(1);
         }
         
         // try to consider filenames starting with / as current from PROPATH entries
         if (len > 0 && filename.charAt(0) == JAR_SEP)
         {
            filename = filename.substring(1);
         }
         
         // walk jars marked as resource jars in the directory
         for (String path : propathSupplier.get())
         {
            String jarFullName = resolveJarName(logger, path, true);
            
            // have the valid jar name
            if (jarFullName != null)
            {
               // get jar file loader from cache
               jarClassLoader = JarClassLoader.getOrCreateCachedJarClassLoader(jarFullName, cl.getParent());
               
               // not possible to get jar file, may be the wrong name?
               if (jarClassLoader == null)
               {
                  continue;
               }
               
               resName = Utils.getRefinedPathName(filename, JAR_SEP_STR);
               
               // first try the search with no PROPATH modifications
               resName = searchFn.apply(jarClassLoader).apply(resName);
               
               if (resName == null && propathSearchFn != null)
               {
                  resName = propathSearchFn.apply(jarClassLoader).apply(filename);
               }
               
               if (resName != null)
               {
                  return new JarResource(jarClassLoader, resName);
               }
            }
         }
      }
      
      return null;
   }

   /**
    * Resolve the jar name and return it as an unique name. If the file does
    * not exist, it is a directory or its canonical path can not be determined,
    * then this method returns <code>null</code>.
    * 
    * @param   jarName
    *          The jar name.
    * @param   fullPath
    *          <code>true</code> if the <code>jarName</code> contains also
    *          the jar's path.  When <code>false</code>, it is assumed that
    *          the path is the one specified in the "customer_libs" system
    *          property.
    *          
    * @return  See above.
    */
   public static String resolveJarName(CentralLogger logger, String jarName, boolean fullPath)
   {
      String absoluteJarPath;
      if (!fullPath)
      {
         String managedDir = MultiClassLoader.managedCustomerLibs();
         if (managedDir == null)
         {
            if (logger.isLoggable(Level.SEVERE))
            {
               logger.log(Level.SEVERE, "Managed customer libraries directory is not specified");
            }

            return null;
         }

         absoluteJarPath = managedDir + jarName;
      }
      else
      {
         absoluteJarPath = jarName;
      }

      try
      {
         return resolveJarName(absoluteJarPath);
      }
      catch (IOException e)
      {
         logger.log(Level.SEVERE, e.getMessage());
         return null;
      }
   }
   
   /**
    * Returns a Set of resolved paths to the jars in the java class path. CentralLogger should be 
    * <code>null</code> if called from ClassLoader, otherwise causes recursion since the loader is still 
    * not initialized.
    *
    * @param   logger
    *          <code>null</code> if called from ClassLoader, CentralLogger instance otherwise.
    * 
    * @return  See above.
    */
   public static List<String> resolveClasspathJars(CentralLogger logger)
   {
      List<String> classpathJars = new ArrayList<>();
      
      String cp = System.getProperty("java.class.path");
      for (String path : cp.split(File.pathSeparator))
      {
         if (!path.endsWith(".jar"))
         {
            continue;
         }

         try
         {
            String pathToJar = JarUtil.resolveJarName(path);
            if (classpathJars.contains(pathToJar))
            {
               continue;
            }
            classpathJars.add(pathToJar);
         }
         catch (IOException e)
         {
            if (logger == null)
            {
               e.printStackTrace();
            }
            else
            {
               logger.log(Level.FINE, "Couldn't resolve jar " + path, e);
            }
         }
      }
      return classpathJars;
   }

   /**
    * Get the actual jar name from the given path.
    * 
    * @param   path
    *          The jar name, with full path.
    *          
    * @return  See above.
    */
   public static String prettyName(String path)
   {
      if (path.contains(File.separator))
      {
         path = path.substring(path.lastIndexOf(File.separator) + 1);
      }
      
      return path;
   }

   /**
    * Extract the jar name from the given URL.
    *
    * @param   logger
    *          The logger.
    * @param   url
    *          The jar URL.
    *          
    * @return  See above.
    */
   public static String extractJarName(CentralLogger logger, URL url)
   {
      String name = resolveJarName(logger, url.getFile(), true);

      return name == null ? null : prettyName(name);
   }

   /**
    * Returns the string of this abstract pathname to the found jar file.
    *
    * @param   absolutePathToJar
    *          The absolute path to the jar file.
    *
    * @return  See above.
    *
    * @throws  IOException
    *          If the path doesn't exist or is not a file.
    */
   private static String resolveJarName(String absolutePathToJar) throws IOException
   {
      File f = new File(absolutePathToJar);
      if (!f.exists() || f.isDirectory())
      {
         throw new IOException("Could not find file for jar " + absolutePathToJar
                                  + ": file does not exist or is not an ordinary file");
      }

      return f.getCanonicalPath();
   }

   /**
    * Object returned by a search for a resource in a jar file.
    */
   public static class JarResource
   {
      /** Class loader for the jar */
      private final JarClassLoader loader;
      
      /** Fully qualified name of the resource in the jar file */
      private final String name;
      
      /**
       * Constructor.
       * 
       * @param   loader
       *          Class loader for the jar.
       * @param   name
       *          Fully qualified name of the resource in the jar file.
       */
      public JarResource(JarClassLoader loader, String name)
      {
         this.loader = loader;
         this.name = name;
      }
      
      /**
       * Get the jar class loader.
       * 
       * @return  Jar class loader.
       */
      public JarClassLoader getLoader()
      {
         return loader;
      }
      
      /**
       * Get the fully qualified name of the resource in the jar file.
       * 
       * @return  Resource name.
       */
      public String getName()
      {
         return name;
      }
   }
}