content: /*
   * Licensed to the Apache Software Foundation (ASF) under one or more
   * contributor license agreements.  See the NOTICE file distributed with
   * this work for additional information regarding copyright ownership.
   * The ASF licenses this file to You under the Apache License, Version 2.0
   * (the "License"); you may not use this file except in compliance with
   * the License.  You may obtain a copy of the License at
   *
   *     http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  package com.alibaba.compileflow.engine.core.infrastructure.utils;
  
  import org.apache.commons.lang3.ArrayUtils;
  import org.apache.commons.lang3.StringUtils;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  import java.io.File;
  import java.io.IOException;
  import java.io.UnsupportedEncodingException;
  import java.net.JarURLConnection;
  import java.net.URL;
  import java.net.URLDecoder;
  import java.util.Enumeration;
  import java.util.HashSet;
  import java.util.Set;
  import java.util.jar.JarEntry;
  import java.util.jar.JarFile;
  
  /**
   * A utility class for finding and loading all classes within a given Java package.
   * <p>
   * This utility can scan for classes in both file-based classpath entries (like in an IDE)
   * and JAR file-based entries. It is useful for dynamically discovering plugin implementations
   * or other components at runtime.
   *
   * @author yusu
   */
  public class PackageUtils {
  
      private static final Logger LOGGER = LoggerFactory.getLogger(PackageUtils.class);
  
      private static final boolean RECURSIVE = true;
  
      private static final String FILE = "file";
  
      private static final String JAR = "jar";
  
      private static final String CLASS_SUFFIX = ".class";
  
      /**
       * Scans all classes accessible from the context class loader under the given package.
       *
       * @param packageName The base package to scan (e.g., "com.example.plugins").
       * @return A set of {@link Class} objects found within the package.
       */
      public static Set<Class<?>> getAllClassInPacakge(String packageName) {
          Set<Class<?>> classes = new HashSet<>();
          if (StringUtils.isEmpty(packageName)) {
              return classes;
          }
          String packagePath = packageName.replace('.', '/');
  
          URL[] urls = ClassLoaderUtils.getResources(packagePath);
          if (ArrayUtils.isEmpty(urls)) {
              return classes;
          }
  
          for (URL url : urls) {
              String protocol = url.getProtocol();
              if (FILE.equals(protocol)) {
                  String filePath = null;
                  try {
                      filePath = URLDecoder.decode(url.getFile(), "UTF-8");
                  } catch (UnsupportedEncodingException e) {
                      LOGGER.error("Failed to decode file: path={}, errorType={}, message={}", url.getFile(), e.getClass().getSimpleName(), e.getMessage(), e);
                  }
                  if (filePath == null) {
                      continue;
                  }
                  scanClassInDir(classes, packageName, filePath, RECURSIVE);
              } else if (JAR.equals(protocol)) {
                  scanClassInJar(classes, packageName, url);
              }
          }
  
          return classes;
      }
  
      /**
       * Recursively scans a directory to find and load classes.
       *
       * @param classes     The set to which loaded classes will be added.
       * @param packageName The Java package name corresponding to the current directory.
       * @param filePath    The file system path of the directory to scan.
       * @param recursive   If true, subdirectories will also be scanned.
       */
      private static void scanClassInDir(Set<Class<?>> classes, String packageName, String filePath,
                                         boolean recursive) {
          File dir = new File(filePath);
          if (!dir.exists() || !dir.isDirectory()) {
              return;
          }
          File[] files = dir.listFiles(file -> (recursive && file.isDirectory())
                  || (file.getName().endsWith(CLASS_SUFFIX)));
          if (null == files || files.length == 0) {
              return;
          }
          for (File file : files) {
              if (file.isDirectory()) {
                  scanClassInDir(classes, packageName + "." + file.getName(),
                          file.getAbsolutePath(), recursive);
              } else {
                  String className = file.getName().substring(0,
                          file.getName().length() - CLASS_SUFFIX.length());
                  String classFullName = packageName + '.' + className;
  
                  try {
                      Class<?> clazz = ClassLoaderUtils.loadClass(classFullName);
                      classes.add(clazz);
                  } catch (ClassNotFoundException e) {
                      LOGGER.error("Class not found: className={}, errorType={}, message={}",
                              classFullName, e.getClass().getSimpleName(), e.getMessage(), e);
                  }
              }
          }
      }
  
      /**
       * Scans a JAR file to find and load classes within a specific package.
       *
       * @param classes     The set to which loaded classes will be added.
       * @param packageName The Java package name to search for.
       * @param url         The URL of the JAR file.
       */
      private static void scanClassInJar(Set<Class<?>> classes, String packageName, URL url) {
          String package2Path = packageName.replace('.', '/');
  
          JarFile jar;
          try {
              jar = ((JarURLConnection) url.openConnection()).getJarFile();
          } catch (IOException e) {
              LOGGER.error("Failed to get jar file: url={}, errorType={}, message={}", url, e.getClass().getSimpleName(), e.getMessage(), e);
              return;
          }
  
          Enumeration<JarEntry> entries = jar.entries();
          while (entries.hasMoreElements()) {
              JarEntry entry = entries.nextElement();
              String name = entry.getName();
              if (!name.startsWith(package2Path) || !name.endsWith(CLASS_SUFFIX) || entry.isDirectory()) {
                  continue;
              }
              String fileFullName = name.replace('/', '.');
              String classFullName = fileFullName.substring(0, fileFullName.length() - CLASS_SUFFIX.length());
              try {
                  Class<?> clazz = ClassLoaderUtils.loadClass(classFullName);
                  classes.add(clazz);
              } catch (ClassNotFoundException e) {
                  LOGGER.error("Class not found: className={}, errorType={}, message={}",
                          classFullName, e.getClass().getSimpleName(), e.getMessage(), e);
              }
          }
      }
  
  }
  