ClasspathScanner.java

package io.vanillabp.integration.utils;

import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;

import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.util.ClassUtils;
import org.springframework.util.SystemPropertyUtils;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class ClasspathScanner {

  private static final Map<String, Resource[]> cache = new HashMap<>();

  private ClasspathScanner() {
    // static class: hide public constructor
  }

  private static ResourcePatternResolver getResourcePatternResolver(
      final ResourceLoader resourceLoader) {
    if (resourceLoader == null) {
      return new PathMatchingResourcePatternResolver();
    } else {
      return new PathMatchingResourcePatternResolver(resourceLoader);
    }
  }

  @SafeVarargs
  public static List<Resource> allResources(
      final Predicate<Resource>... filters) throws Exception {

    return allResources(null, null, filters);

  }

  @SafeVarargs
  public static List<Resource> allResources(
      final ResourceLoader resourceLoader,
      final Predicate<Resource>... filters) throws Exception {

    return allResources(resourceLoader, null, filters);

  }

  @SafeVarargs
  public static List<Resource> allResources(
      final String basePath,
      final Predicate<Resource>... filters) throws Exception {

    return allResources(null, basePath, filters);

  }

  @SafeVarargs
  public static List<Resource> allResources(
      final ResourceLoader resourceLoader,
      final String basePath,
      final Predicate<Resource>... filters) throws Exception {

    final var searchPath = "%s%s/**/*".formatted(
        ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX,
        basePath == null ? "" : basePath);

    final Resource[] resources;
    synchronized (cache) {
      final var cachedResources = cache.get(searchPath);
      if (cachedResources != null) {
        resources = cachedResources;
      } else {
        resources = getResourcePatternResolver(resourceLoader).getResources(searchPath);
        cache.put(searchPath, resources);
      }
    }

    final List<Resource> result = new LinkedList<>();

    for (final var resource : resources) {
      if (resource.isReadable()) {
        boolean complies = true;
        for (Predicate<Resource> filter : filters) {
          if (!filter.test(resource)) {
            complies = false;
            break;
          }
        }
        if (complies) {
          result.add(resource);
        }
      }
    }

    return result;

  }

  @SafeVarargs
  public static List<Class<?>> allClasses(
      final String basePackage,
      final Predicate<MetadataReader>... filters) throws Exception {

    return allClasses(null, basePackage, filters);

  }

  @SafeVarargs
  public static List<Class<?>> allClasses(
      final ResourceLoader resourceLoader,
      final String basePackage,
      final Predicate<MetadataReader>... filters) throws Exception {

    final var packageSearchPath = "%s%s/**/*.class".formatted(
        ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX,
        resolveBasePackage(basePackage));

    final List<Class<?>> classes = new LinkedList<>();

    final var resourcePatternResolver = getResourcePatternResolver(resourceLoader);

    final Resource[] resources;
    synchronized (cache) {
      final var cachedResources = cache.get(packageSearchPath);
      if (cachedResources != null) {
        resources = cachedResources;
      } else {
        resources = resourcePatternResolver.getResources(packageSearchPath);
        cache.put(packageSearchPath, resources);
      }
    }

    final var metadataReaderFactory = new CachingMetadataReaderFactory(resourcePatternResolver);
    for (Resource resource : resources) {
      if (resource.isReadable()) {
        try {
          final var metadataReader = metadataReaderFactory.getMetadataReader(resource);
          boolean complies = true;
          for (Predicate<MetadataReader> filter : filters) {
            if (!filter.test(metadataReader)) {
              complies = false;
              break;
            }
          }
          if (complies) {
            try {
              final var c = Class.forName(metadataReader.getClassMetadata().getClassName());
              classes.add(c);
            } catch (Throwable e) {
              log.trace("Class not found: {}", metadataReader.getClassMetadata().getClassName());
            }
          }
        } catch (NoClassDefFoundError e) {
          log.debug("NoClassDefFoundError: it might be an optional dependency", e);
        }
      }
    }

    return classes;

  }

  private static String resolveBasePackage(
      final String basePackage) {

    return ClassUtils.convertClassNameToResourcePath(SystemPropertyUtils.resolvePlaceholders(basePackage));

  }

}